Python Programming in a UNIX-like Environment

We’ve now explored computers from the perspective of a “power user”—someone who knows the tools of an operating system and can either leverage them or combine them in novel ways to accomplish their goals.

So far the tools we’ve encountered have been purpose-built tools: programs like ls, mkdir, and touch each have a single well-defined purpose. The touch program adjusts timestamps to create new files. The mkdir program creates directories. But the true power of a computer is not its ability to execute purpose-built tools—those have existed for nearly the same amount of time as humans have existed. The true power of a computer is that they are general-purpose tools: a computer is a tool that can be re-purposed, re-tooled, or re-programmed to accomplish any task which can be described by an algorithm, where an algorithm is a discrete set of steps needed to make a decision.

We will use Python (a programming language), which is one such general-purpose tool which we will use to implement algorithms and construct new purpose-built tools. Since our Python programs will inevitably run on a Unix-like operating system, we spend this lesson:

  • Starting and stopping programs to familiarze ourselves with the Unix process model
  • Writing Python in a REPL (sometimes called interactive mode), then loading and running programs with the Python interpreter (sometimes called batch mode or batch processing)

Follow Along with the Instructor

We’ll talk through some points from the material, talk through steps of the function design recipe, and implement a rock-paper-scissors game that can be played from the command-line.

Starting and Stopping Programs

Every topic from here starts from a terminal.

What happens if we create a new file—perhaps: always_true.py

touch always_true.py

… and add the following code to it?

while True:
    pass

On its own: nothing. But what if we call upon the python3 interpreter to run that code?

$ python3 always_true.py

Is something happening? Is nothing happening? The machine is doing exactly what we told it to do. We asked it to stay inside that loop forever, which it will do until the program crashes (unlikely), our computer shuts off (which includes running out of battery), or we tell our operating system to interrupt the program.

We can send SIGINT, or the interrupt signal, with the ^ Ctrl + C shortcut.1

$ python3 always_true.py
^C
$

Here is what this shows us:

  • we can start a program
  • we can wait for that program to complete
  • if the program does not halt, there is a bigger and more complex program—an operating system—which we can use to stop another program

Analogy: Task Manager and Activity Monitor

Perhaps something on Microsoft® Windows® went poorly for you in the past, so you got out Ol’Reliable: ^ Ctrl + Alt + Delete, click the “Task Manager” button, then “End Task” the misbehaving program.

Those steps form a direct analogue of sending a SIGINT using ^ Ctrl + C in the terminal. The Windows and macOS desktop operating systems are far-removed from the Linux and Unix-like environments we’re working with, but users of those systems share many of the same needs. Eventually something will go wrong, and users will need a tool that stops an erratic behavior.

Whereas Windows Desktop exposes one familiar tool: Task Manager; modern GNU/Linux/Unix operating systems provide at least five:

SIGTERMterminate
SIGINTinterrupt
SIGQUITquit & (usually) produce a core dump
SIGKILLthe nuclear option
SIGHUP“hang up”, indicating a connection was lost

The details of these are out-of-scope here, and you can read about them when you’re working at a low-enough level of abstraction to need them.1

Instead, here are three concepts you can use immediately: PID, top, and kill.

  • a process id (PID) is a number that the operating system assigns to every process
  • the top shows you processes, as well as their PID numbers
  • kill takes a PID and kills the program

Taken together, if you search and kill a misbehaving program:

$ top | grep 'python3'
145633 hayesall  ... python3
$ kill 145633

Then the other terminal running the misbehaving program will report its death:

>>> while True:
...     pass
...
[1]    145633 terminated  python3
$

A programming language is also a program

Prior to this we used simple programs: ls, cd, mkdir, touch. What happens when you type ls into your terminal and hit ↵ Enter?

The ls command should show you some files and directories. Showing nothing just means there are neither files nor directories. But what happens if you type python3 and hit ↵ Enter?

$ python3

Hopefully (assuming Python is installed) you get something similar to:

$ python3
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

This is a REPL, an acronym for “Read, Eval, Print, Loop” (read something from the user, evaluate the expression, print the result of that expression, then loop back to the read step). The REPL gives us a place to type bits of Python code, hit ↵ Enter, and somehow that translates into something happening somewhere on our computer. This can be an extremely useful tool in your toolbox when you’re sketching out a new idea.

The humble REPL is the place we begin and it is the place we will return to many times. The REPL shows us a principle that separates simple programs which may be explained in terms of a fixed number of simple operations (print files, create a new file, make a directory), from non-simple programs which strive toward the infinite.

In the next few weeks: we’ll aim our sights toward writing programs that intentionally continue forever, or at least until someone turns them off.

We just learned about ^ Ctrl + C. Can we use that to interrupt a Python REPL?

>>>
KeyboardInterrupt
>>>
KeyboardInterrupt
>>>

Sort of. SIGINT is a signal—it’s a message from us to the program. It is up to the receiver to listen for that message and interpret its meaning. The interpretation of that message depends on where we are: are we in a Python REPL, or a Terminal Shell?

When we’re in a Python REPL, sending SIGINT appears to print the word KeyboardInterrupt and send us back to the read step. Since we can interract with Python via its REPL, there’s another place the interrupt is reserved to help us with:

>>> while True:
...     pass
...
^CTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyboardInterrupt
>>>

Do you see it? The SIGINT ^C is right there before the Traceback begins.

When in the REPL: • Use ^C to get back to the REPL prompt • Use ^D to get back to the Terminal prompt

Designing Programs

Programming language are built from five essential components.2

Variablesstore a value for later use. x = 1
Conditionalschoose a behavior based on an observation. if, elif, else
Repetitionrepeat a procedure until some condition is met. for, while
Abstractionencapsulate a behavior; hide the details. def, class, import
Applicationinvoke an abstraction to return a result. x + 1

Every complex program—operating systems, video games, machine learning models, space shuttles—is at some low level of abstraction doing all five of these things. Major innovations happened over the last fifty years that made computers faster, smaller, and more affordable; but the core operation of transforming data is still here.

In “How to Design Programs”, Felleisen et al. define a “systematic program design” approach as the following six steps. When you’re working alone, these can guide you toward a solution. When you’re working with other agents—prompting large language models (LLMs) or asking someone for guidance—these can communicate where your thoughts are and how you organize ideas.

The Function Design Recipe 🥣

The “How to Design Programs” systematic design steps:3

  1. From Problem Analysis to Data Definitions. Identify the information that must be represented and how it is represented in the chosen programming language. Formulate data definitions and illustrate them with examples.
  2. Signature, Purpose Statement, Header. State what kind of data the desired function consumes and produces. Formulate a concise answer to the question what the function computes. Define a stub that lives up to the signature.
  3. Functional Examples. Work through examples that illustrate the function’s purpose.
  4. Function Template Translate the data definitions into an outline of the function.
  5. Function Definition. Fill in the gaps in the function template. Exploit the purpose statement and the examples.
  6. Testing. Articulate the examples as tests and ensure the function passes all. Doing so discovers mistakes. Tests also supplement examples in that they help others read and understand the definition when the need arises—and it will arise for any serious problems.

Rock-Paper-Scissors

Rock-paper-scissors is a schoolyard game played between two opponents. On each round: players secretly decide whether they will choose rock, paper, or scissors; the winner is then decided based upon the narrative that “paper covers rock”, “rock crushes scissors”, and “scissors cuts paper”.

Check GitHub for your username-i211-starter repository, and clone a copy to your local machine. We’ll be using this repository for the next few lessons (replacing USERNAME with your username):

git clone https://github.iu.edu/i211su2024/USERNAME-i211-starter.git

cd into it:

cd USERNAME-i211-starter

and open the folder in Visual Studio Code:

code .

Working in Visual Studio Code

Visual Studio Code (VS Code) is an open source text editor by Microsoft, which will serve as our editor of choice for the remainder of this book. As of the 2023 StackOverflow developer survey, it was one of the most popular editors (or integrated development environments) with 73.71% of developers listing it as their primary editor.4 Our nano skills are still useful—in the same way we said that most computers are servers that lack a graphical desktop: there are far more computers with nano installed than there are with code installed.

However: we will always recommend starting from a Terminal. Once you’re comfortable with where files and folders exist on your operating system—then it will be fine to experiment with some of the higher-level buttons.

Plenty of online tutorials for VS Code already exist—including official ones created by people at Microsoft. The Introductory Videos playlist is a pretty good place to start for something more in-depth. This covers alternative approaches to topics like breakpoint debugging or version control in a slightly different way.

We do strongly recommend spending some time getting comfortable using a text editor like Visual Studio Code. Some of the common operations when writing code include general concepts like “moving around”, “selecting text”, “editing text”, and “managing windows”.

Refer back to the following tables, which list many frequent shortcuts for Windows, Linux, and ChromeOS (macOS: usually substitute ^ Ctrl for ⌘ Cmd). or review keyboard shortcuts directly inside VS Code by opening the command palette with ^ Ctrl + ⇧ Shift + P.

Moving around and selecting generally involves arrow keys or navigation keys:

ShortcutDescription
arrow keysmove cursor up, down, left, right
^ Ctrl + left/right arrowmove cursor one word left/right
^ Ctrl + up/down arrowscroll up/down
PgUpScroll up one page
PgDownScroll down one page
⇧ Shift + arrowselect text
^ Ctrl + ⇧ Shift + left/rightmulti-word select
^ Ctrl + ASelect everything in document
HomeStart of line
EndEnd of line
⇧ Shift + HomeSelect to start of line
⇧ Shift + EndSelect to end of line
^ Ctrl + HomeStart of file
^ Ctrl + EndEnd of file

General editing such as save, cut, copy, paste, indent, comment, undo, redo:

ShortcutDescription
^ Ctrl + SSave file
^ Ctrl + ZUndo
^ Ctrl + YRedo
^ Ctrl + ASelect all
^ Ctrl + CCopy selection to clipboard
^ Ctrl + XCut selection to clipboard
^ Ctrl + VPaste from clipboard
Tab ↹Indent selection
⇧ Shift + Tab ↹Un-Indent selection
^ Ctrl + /Comment-out selection

Tab and window management is helpful as soon as we have more than one document. Be mindful that by the end of this course our projects will have 30+ files:

ShortcutDescription
^ Ctrl + ⇧ Shift + PToggle “command palette”
^ Ctrl + BToggle left sidebar
^ Ctrl + ⇧ Shift + EOpen “file explorer”
^ Ctrl + ⇧ Shift + XOpen “extensions menu”
^ Ctrl + `Toggle between code and terminal
^ Ctrl + ⇧ Shift+ `Open new terminal
^ Ctrl + ⇧ Shift + 5Split terminal vertically
^ Ctrl + PgDownNext terminal
^ Ctrl + PgUpPrevious terminal
^ Ctrl + PToggle “quick open”
^ Ctrl + WClose current tab
Alt + 1 (through 9)Jump to tab 1, 2, 3, …
^ Ctrl + 1 (through 9)Jump to tab group 1, 2, 3, …
^ Ctrl + Tab ↹Next tab
^ Ctrl + ⇧ Shift + Tab ↹Previous tab
^ Ctrl + \Split editor
⇧ Shift + Alt + 0Swap horizontal/vertical layout

What subset of Python do we need?

Python does a lot—you might review the Python refresher if it’s been a while.

We’ll definitely need the core of the language: variables, conditions (if, elif, else), loops, functions, and function application. But a few built-in functions, input/output handling, and random number generation (import random) will be needed to.

The built-in input() function is a quick way to get information from the user in the middle of program evaluation, and we can use this to solicit what they want to play:

>>> x = input("scissors/paper/rock> ")
scissors/paper/rock> rock
>>> x
'rock'

So that will handle the human player. How would we build an opponent for them to play against? When you have no prior knowledge about the opponent you’re playing against: the best strategy is to behave randomly. Implementing our own random number generator is outside what we want to cover, but the Python standard library includes a module called random to assist with these tasks:

>>> from random import choice
>>> choice(["scissors", "paper", "rock"])
'paper'
>>> choice(["scissors", "paper", "rock"])
'rock'
>>> choice(["scissors", "paper", "rock"])
'paper'
>>> choice(["scissors", "paper", "rock"])
'paper'

Finally, we should program defensively to guard against bad data getting into our system. But if it does, we should provide feedback on how to correct an improper action. Every program in a Linux system has two kinds of output: standard output (STDOUT) and standard error (STDERR).

Python has a built-in print() function. By default, the function sends output to STDOUT5 using sys.stdout. But this can be changed using the sys.stderr file:

>>> from sys import stderr
>>> human_choice = "cannon"
>>> print(f"Unknown value: '{human_choice}'", file=stderr)
Unknown value: 'cannon'

Goal: Implement RPS

Implement a version of Rock-Paper-Scissors. It should:

  1. Ask the user to choose an option, possibly many times if they have typos. In the event of a problem: send an explanation to STDERR.
  2. Choose a random action for the computer.
  3. Display the winner: human or computer?

Starting from rps.py, fill in the gaps with incremental development where you periodically run the code to see how it works:

from random import choice
from sys import stderr


def is_valid(raw: str) -> bool:
    pass


def main():
    pass


if __name__ == "__main__":
    main()

A complete program might behave similar to this:

$ python3 rps.py
(scissors/paper/rock) >>
$ python3 rps.py
(scissors/paper/rock) >> cannon
Unknown: 'cannon', try again

(scissors/paper/rock) >>
$ python3 rps.py
(scissors/paper/rock) >> rock
Computer chose 'paper'
Computer wins!
$

Practice incremental development. If you get stuck inside the program’s execution, recall that you can send the SIGINT to get back to your shell. If you’re uncertain about an internal state of the program: print debugging is a technique where you add “print statements” to give yourself feedback about about some intermediate program state—just remember to delete them when you’re finished.

How did it go?

Easy 😌. Cool!

Hard 😓. That’s okay. Here’s what I want you to do: get a good night’s sleep and try this exercise again tomorrow. You will come back with fresh eyes and the experience you gained last time. Keep doing this everyday until you see parentheses in your dreams.

I didn’t do it 😳. Go back and try again. Seriously. The only way to learn is by doing. If your plan is to read about solutions then copy & paste: you will not pass this course.

The “Soft Skills” of Software: How to make yourself (and anyone who works with you) miserable

So great: I’ll assume you’re reading this because your Rock-Paper-Scissors implementation is working.

We aren’t ready to explore “The Answer” yet. Since this course assumes you’ve already had at least one or more semesters of programming experience: we instead want to reflect on the code we wrote for RPS, and perhaps on all the code that we wrote previously.

So in this section, we’re going to talk about some “red flags” that come up when reading code.

We’ll use a listing like this one to illustrate problems from time to time. Can you spot a few “red flags” in this code? How might you improve it?

#                   functions:
#                  -----------------------

# TODO no longer needed, see the version in 'new_stingify_kxt.py'
# Define the list to string function
def list_to_string(input_list):
    """Author: Alexander L. Hayes"""
    strd = str(input_list)              # Convert to a string
    middle = strd[1:-1]                 # Take the middle elements
    # print("--- Line 16")
    # print(middle)

    for s in middle:        # Added by KXT for debugging
        print(s)

    return middle.replace(", ", " ")    # Replace comma with space

# Print the list_to_string
print(list_to_string([1, 2, 3]))        # Use [1, 2,3 ] as example

🚩 Comments

Bad advice: “Make sure to add as many comments as possible!

Advice: Strive to write code that doesn’t need any.

Why? Comments lie, and there are few things more harmful than reading an out-of-date comment; or online commentary that is no longer valid. Do you want to know what doesn’t lie as often? Code that is routinely validated and tested for correctness. There is a flavor of comments that are closer to code, but they’re called docstrings. More on those later.


🚩 Commented-out code

Bad advice: “You Might Need It Later!

Advice: No you won’t. Delete it.

Why? We spent several lessons talking about version control systems: tools that allow you to store, transmit, and time travel to any point in history. If you actually do need it later: it’s back there in the history. If it’s something important and you’re worried you’ll forget it: the current file is not the right place for that. Learn to take better notes and refer back to them later.

Need an intermediate solution? Create a new file called notes.py and store any code you think you might need later in it. Just like how your parents probably moved all your little kid toys to the basement, waited until you forgot about them, and then donated them (“Oh I don’t know what happened to that honey 🤷🏻‍♀️”), you’re going to find you probably DON’T need that code. When you’re ready, delete the file. (And shhh don’t tell Erika’s son about his toys.)


🚩 Zero Functions

Bad advice: “Functions: the fewer, the better!

Advice: If you can name it: make it a function.

Abstraction” is the fundamental tool of computing. Designing a good abstraction is hard, but here’s the trick: we need to be on the lookout for them, and leave space for when they do appear.


🚩 Write it all in the main

Bad advice: “Just write everything in the main!

Advice: Writing code “in the main” is useful during rapid prototyping. As you progress: convert your insights into functions.

If you are not familiar with the phrase “in the main”, it’s possible that the concept was not shown to you when you previously learned programming. Contrast the following two programs. This program puts most of its behavior inside of a function called itersum, and all user-facing behavior is defined using a main guard:

def itersum(lst: list[int]):
    out: int = 0
    for e in lst:
        out += e
    return out

if __name__ == "__main__":
    print(itersum([1, 2, 3]))
Case 1: a program where most behavior is inside of an itersum function. All relevant inputs and outputs to the program are defined in a main guard which clearly delineates what the inputs and outputs are.

By contrast, this program:

# main guard shown here for emphasis. Removing the `if`
# statement and tabbing the code left is nearly identical.
if __name__ == "__main__":
    out = 0
    lst = [1, 2, 3]
    for e in lst:
        out += e
    print(out)
Case 2: this program produces the same result as the former, but there is no separation between inputs and outputs. This is what we mean when we say that everything was defined in the main.

… makes no clear distinction between inputs and outputs. The input is left implicit, and one must manually trace the full program’s logic to arrive at why some output is the consequence of some input.

Here’s the key point: running both programs produces the same result. The result is objectively the same, but which program is going to be easier to read and extend?

As you develop your development skills, your __main__ sections should grow smaller and smaller. When we’re ready to write Flask servers in a few weeks: our main block will be a single line of code:

if __name__ == "__main__":
    app.run()

A possible rock-paper-scissors solution

Many students that I (Alexander) have taught want to know the solution to a problem. So here I must caution you: The solution does not exist. What is presented here is “one possible solution of many”, not the Solution-with-a-capital-S.

Here are some rough steps to help you learn:

  1. Find a problem
  2. Try to solve it
  3. Compare your solution to someone else’s solution
  4. Identify the differences between the two
  5. Ask yourself some questions about those differences
    • Do you like a choice better?
    • Is there some new idea I can use?

Try these five steps with your solution and my solution:

from sys import stderr
from random import choice


def is_valid(raw: str) -> bool:
    """Is the raw input a valid choice?"""
    return raw in ("rock", "paper", "scissors")


def beats(this: str, that: str) -> bool:
    """Does `this` beat `that`?"""
    return (this, that) in (
        ("rock", "scissors"),
        ("paper", "rock"),
        ("scissors", "paper"),
    )


def get_computer_choice() -> str:
    """Choose from (rock, paper, or scissors)"""
    return choice(("rock", "paper", "scissors"))


def get_human_choice() -> str:
    """Ask the"""
    while True:
        if is_valid(human := input("(rock/paper/scissors) >> ")):
            break
        print(f"Unknown {human}, try again", file=stderr)
    return human


def main():
    """Play a full game of rock/paper/scissors"""
    human = get_human_choice()

    computer = get_computer_choice()
    print(f"Computer chose '{computer}'")

    if human == computer:
        print("It's a tie!")
    elif beats(human, computer):
        print("Human wins!")
    else:
        print("Computer wins!")


if __name__ == "__main__":
    main()

Step 1 in the Function Design Recipe suggests that we start by identifying the data we need to represent. Our inverted game of rock-paper-scissors needs to represent data about what beats what? Jumping to Steps 5, the beats(this, that) answers whether this beats that:

def beats(this: str, that: str) -> bool:
    """Does `this` beat `that`?"""
    return (this, that) in (
        ("rock", "scissors"),
        ("paper", "rock"),
        ("scissors", "paper"),
    )

How did we arrive at something like this? By thinking through examples for how we would use such a function (Steps 2-3):

>>> beats("scissors", "rock")
True
>>> beats("paper", "rock")
False

Were there other ways we could have done this? Absolutely! Maybe you find functions to be unnecessary here, and instead chose to encode the core problem of “what beats what” as a chain of if/elif-statements:

if human == computer:
    print("It's a tie!")
elif human == "paper" and computer == "rock":
    print("Human wins!")
elif human == "scissors" and computer == "paper":
    print("Human wins!")
elif human == "scissors" and computer == "rock":
    print("Human wins!")
else:
    print("Computer wins!")

Is this also a valid solution?

Nope. Can you spot the bug that I left in the chain of if/elif/else statements?

Here’s the point: Alexander has been programming for a while. After writing a few million lines of code, he learned to be suspicious of towers of if-statements: too often they caused his eyes glaze over, or else he’d have to exert mental energy checking every case before deciding the whole was sound.

Next Time

We skipped Step 6: we did not talk about testing our code, or any way to think about correctness.

Further Reading

  • Matthias Felleisen, Robert Bruce Findler, Matthew Flatt, and Shriram Krishnamurthi, (2014) “How to Design Programs: An Introduction to Programming and Computing” (Second Edition). The MIT Press.

Footnotes

2

These five follow from a procedural approach to programming and programming languages. Other paradigms exist which may appear to bend these rules—such as structured query language (SQL), which is an instance of a declarative language. A lambda calculus approach to studying languages would tell you that all computation can actually be done with three rules: definition, abstraction, and application—the astute reader may wonder where concepts like conditions and repetition went? The answer is that those concepts can just as easily be defined in terms of abstraction and application.

3

From: Felleisen et al. 2014, “How to Design Programs”. Used under the terms of the Creative Commons CC BY-NC-ND license. Online: HTDP, Preface, Systematic Program Design

4

“2023 Developer Survey”, StackOverflow, (May 2023), https://survey.stackoverflow.co/2023/#integrated-development-environment, (accessed 2024-07-09).

5

Python 3.12.3 Documentation, “Built-in Functions: print”. accessed: 2024-05-02. Online: https://docs.python.org/3/library/functions.html#print