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:
SIGTERM terminate SIGINT interrupt SIGQUIT quit & (usually) produce a core dump SIGKILL the 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
, andkill
.
- a process id (PID) is a number that the operating system assigns to every process
- the
top
shows you processes, as well as theirPID
numberskill
takes a PID and kills the programTaken 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
Variables | store a value for later use. x = 1 |
Conditionals | choose a behavior based on an observation. if , elif , else |
Repetition | repeat a procedure until some condition is met. for , while |
Abstraction | encapsulate a behavior; hide the details. def , class , import |
Application | invoke 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
- 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.
- 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.
- Functional Examples. Work through examples that illustrate the function’s purpose.
- Function Template Translate the data definitions into an outline of the function.
- Function Definition. Fill in the gaps in the function template. Exploit the purpose statement and the examples.
- 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:
Shortcut | Description |
---|---|
arrow keys | move cursor up, down, left, right |
^ Ctrl + left/right arrow | move cursor one word left/right |
^ Ctrl + up/down arrow | scroll up/down |
PgUp | Scroll up one page |
PgDown | Scroll down one page |
⇧ Shift + arrow | select text |
^ Ctrl + ⇧ Shift + left/right | multi-word select |
^ Ctrl + A | Select everything in document |
Home | Start of line |
End | End of line |
⇧ Shift + Home | Select to start of line |
⇧ Shift + End | Select to end of line |
^ Ctrl + Home | Start of file |
^ Ctrl + End | End of file |
General editing such as save, cut, copy, paste, indent, comment, undo, redo:
Shortcut | Description |
---|---|
^ Ctrl + S | Save file |
^ Ctrl + Z | Undo |
^ Ctrl + Y | Redo |
^ Ctrl + A | Select all |
^ Ctrl + C | Copy selection to clipboard |
^ Ctrl + X | Cut selection to clipboard |
^ Ctrl + V | Paste 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:
Shortcut | Description |
---|---|
^ Ctrl + ⇧ Shift + P | Toggle “command palette” |
^ Ctrl + B | Toggle left sidebar |
^ Ctrl + ⇧ Shift + E | Open “file explorer” |
^ Ctrl + ⇧ Shift + X | Open “extensions menu” |
^ Ctrl + ` | Toggle between code and terminal |
^ Ctrl + ⇧ Shift+ ` | Open new terminal |
^ Ctrl + ⇧ Shift + 5 | Split terminal vertically |
^ Ctrl + PgDown | Next terminal |
^ Ctrl + PgUp | Previous terminal |
^ Ctrl + P | Toggle “quick open” |
^ Ctrl + W | Close 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 + 0 | Swap 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:
- 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.
- Choose a random action for the computer.
- 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]))
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)
… 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:
- Find a problem
- Try to solve it
- Compare your solution to someone else’s solution
- Identify the differences between the two
- 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
Termination Signals. Online: https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html
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.
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
“2023 Developer Survey”, StackOverflow, (May 2023), https://survey.stackoverflow.co/2023/#integrated-development-environment, (accessed 2024-07-09).
Python 3.12.3 Documentation, “Built-in Functions: print”. accessed: 2024-05-02. Online: https://docs.python.org/3/library/functions.html#print