Let's write a program. What is a program? A program really is just a bunch of text. Programs or code live in text files. Whether it's a valid program depends on the programming language that we use. In our case, we are using Python, so our program needs to follow the grammatical rules of Python.
Here is the first program that everyone writes. (Why? This practice is largely due to the influence of the 1978 Kernighan and Ritchie book "The C Programming Language".)
print("Hello world!")
There are a few things to notice here.
print is a built-in function. Its purpose is to take its input and display it in the terminal.
print is the value "Hello world!". This is a string, which in short is text data (more on that later).
print, Print, and pRiNt will all mean and do something different.
Now that we have some code, how do we use it? There are two main ways to execute Python code: by interacting with the interpreter, or by running a Python program.
The interpreter can be run from the terminal by using the command ipython3. You can think of the interpreter as a very, very powerful calculator: you plug in code and it runs it immediately and gives you a result. This is very useful for trying things out and running one or two lines of code.
The other way involves writing an entire program in an editor like Visual Studio Code, saving it as a file, and running that file like a program. Suppose we have a program saved to a file called hello.py. Then to execute this program we can run
python3 hello.py
Here, hello.py is just a text file with Python instructions in it. By convention, we name these files with a file extension of .py.
What is happening here is that we hand the interpreter a file that contains some text. The interpreter will assume that the text follows the grammatical rules of Python and try to follow the instructions in the text as best as it can. If it's not actually a valid Python program, it will fail. Similarly, if an arbitrary file like hello.jpg happens to contain valid Python, then the interpreter will happily execute it.
So far, we've seen exactly one program: display "Hello world" on the terminal. This is not a particularly interesting program, largely because it never changes. When we think of the usefulness of computers and programs, it's their ability to take some input and produce some result based on that input.
Part of solving problems computationally will be in understanding the information being received and produced for your problem and managing its representation as data in your programs. Python has several basic tools and concepts for this purpose.
When a value is stored or represented by the computer, what does it look like? In reality, it's literally just a bunch of ones and zeroes—all of it, whether it's numbers, text, or anything else. So how does a computer interpret what all of these things actually mean?
One way that programming languages deal with this is by assigning types to values. Every value in Python has a type associated with it. Because Python is dynamically-typed, we do not need to tell it what type some value is—it will try to figure it out on its own. (Other languages are statically-typed and require us to explicitly declare types)
Types are important because they describe the possible values a piece of data can take on (i.e. what kind of "thing" is it?) and they dictate what kinds of actions and operations we can apply to a value.
Python has a number of built-in types. Here are a few of the more common ones that we'll be using (these are also found in other programming languages).
int) are the same as integers ($\mathbb Z$) you may remember from math. Because they are whole numbers, they are stored and represented exactly. Integer literals can be created simply by typing them, like -7 or 2434. This is as opposed to...
float), or real numbers, which are also called floating-point numbers. Reals or floats are representations of real numbers, just like you see in math ($\mathbb R$). Unlike integers, representing a real number exactly is impossible. Instead an approximate representation, called floating-point numbers, are used. This is a standard representation across almost all programming languages. Like integers, these float literals can be created by the obvious representation, such as 3.15 or -9.8. Note that to create a float of a whole number, we include the fractional part explicitly: 5.0 is a float while 5 is considered an integer.
str) are text data, which can be interpreted as sequences of characters. String literals are denoted by being surrounded by either single or double quotes. It is important to remember the quotes: both "A good idea" and 'A good idea' are both interpreted as the same string, while A good idea is not recognized as a string (or anything in particular).
bool) are truth values, either True or False. Note that this is case sensitive: true (which will not be recognized) is not the same as True (a boolean value).
Python also has a special value called None which has type NoneType. The value None denotes "no value". This is semantically different from 0 or False.
For example, consider what the difference in meaning may be between a grade of None (no grade assigned) and a grade of 0 (received a grade of 0).
Something about computers that we can take advantage of is the fact that they can remember a lot of things. Where do they remember them? In its memory. So how do we put and get things in memory? As you may know, computers are very complicated machines, so this is very complicated, but luckily we don't actually need to worry about that—the programming language abstracts this process.
Instead of dealing with the memory directly, our programming language uses variables to manage this information. Just as in math, variables give us a way to refer to specific values that we would like to remember. We give a variable a name and we assign a value to it. In Python, one assigns a value to a variable in the following way:
variable_name = <value>
Because of the use of = (a questionable decision from decades ago that we must all now live with), this is very easy to misread as variable_name is equal to or is the same as <value>, but this is not what it means. Rather, we should read this as "let $variable\_name$ be $\langle \mathrm{value} \rangle$". This means that variable_name is taking on the value <value>.
message = "hello world"
Once we've defined a variable, we can use it in other places.
>>> print(message)
"hello world"
Here, Python will evaluate the variable and look up the value of the variable message to use in the print function.
Now, suppose we try to evaluate a variable that we have not defined yet.
>>> print(something_else)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'something_else' is not defined
Because this variable hasn't been defined yet, Python doesn't know what it is and it will tell you. The final line of an error will usually tell you what went wrong and that is usually what you should focus on. Of course, you should take a moment to consider why you're receive that error. For example, if you knew you defined a variable like message but typed something like mesage, you'll get the same error. In this case, you might think that you've already defined your variable, but made a typo.
Unlike what's typical in mathematics, the value of a variable can also change.
>>> a = 0
>>> print(a)
0
>>> a = 2
>>> print(a)
2
Since variables hold values, we can assign variables to the value of some other variable.
>>> b = a
>>> print(b)
2
Suppose we update a after this.
a = 4
What does b evaluate to now?
A common misconception is that the assignment b = a assigns the value of a to b at all times. But this is not the case. Rather, the assignment will always evaluate the right hand side and then perform assignment on the resulting value.
Let's walk through this more carefully.
b = a.
a evaluates to 2, so we have that $b \gets 2$. a = 4 so $a \gets 4$.
a evaluates to 4 and b evaluates to 2.
Suppose we would like to assign the value of a, which is 4 to another variable x and we try the following.
>>> x = print(a)
4
>>> print(x)
Nothing appears! What happened? We mentioned earlier that the function print takes its input and displays it in the terminal. But the function doesn't actually evaluate to any value—all it does is show something on screen. So what happened was we ended up assigning nothing to x. As mentioned above, this is sometimes represented using None, but will often manifest as there being no value at all.
Such instructions or functions, that tell the computer to "do something" but don't evaluate to some value, are called statements. Another example of a statement is assignment: the statement x = 23 doesn't "produce" any value. It tells the computer to store 23 using the variable x.
In contrast to statements, which are instructions that tell the computer to perform some action without producing a value, an expression in Python is an instruction that can be evaluated and produces a value.
Here, expression is evocative of the same concept in math and arithmetic: we use operations and apply them to values to describe more complicated values. Python has a number of basic operations built into it that can be used to construct expressions. For example, since we have number types, the usual arithmetic operations mostly work the way you would expect.
>>> 2 + 3
5
>>> 2 - 3
-1
However, we also have the ability to store values in variables. So we can evaluate these variables and use them with these operations, as well as store the results.
>>> m = 23
>>> n = 34
>>> print(m + n)
57
Multiplication is denoted by *.
>>> 3 * 4
12
Division is denoted by / and behaves slightly differently. Since the integers aren't closed under division, dividing two numbers in Python is defined to always produce a float.
>>> 22 / 7
3.142857142857143
Because of this quirk, there's another division operator, //, which will always produce an integer, specifically the quotient of the result.
>>> 22 // 7
3
These correspond to the two understandings of division you may already have. In the first case, we have division over the real numbers, which will always produce a real number. In the second case, we have division as a function on integers, which will always produce a quotient and a remainder.
With that said, you might notice that // only produces the quotient. If you would also like the remainder, the modulus operator, %, will produce the remainder of the division of its operands.
>>> 22 % 7
1
You can verify that it's the case that // and % together will give you everything you need for integer division.
>>> q = 22 // 7
>>> r = 22 % 7
>>> print(q * 7 + r)
22
One final note is that all of these operations are written infix. This is a bit of a problem because complicated expressions with multiple operations can be ambiguous. Luckily, Python follows the usual established convention for order/precedence of operations:
If two operations have the same precedence, they are evaluated from left to right.
It is also possible to write expressions for strings. For example,
>>> "hot" + "dog"
'hotdog'
This seems strange if you think of $+$ as "addition" on numbers, but we've actually seen an example of an operation working differently based on types: division. Just as we have division for reals and division for integers, we can have addition for numbers and addition for strings. This idea that we can define similar operations for different types of objects is common in algebra.
This operation is called concatenation. Informally, it takes two strings and joins them together. At first glance, it seems like a very arbitrary thing to do, but concatenation is actually a very well-defined operation that has its origins in the study of algebra on strings.
One interesting property that concatenation doesn't share with addition on numbers can be seen in the following example,
>>> "dog" + "hot"
'doghot'
The order of the operands matters in concatenation: $x+y$ is not necessarily the same as $y+x$, which is not what we understand from numeric addition. Formally, addition on numbers is said to be commutative and concatenation on strings is not commutative.
Expressions may also involve data of different types. For instance, suppose we wish to repeatedly concatenate the same string:
>>> "bark" + "bark" + "bark" + "bark" + "bark"
'barkbarkbarkbarkbark'
Mathematically, we can think of this as the "multiplication" of a string. This interpretation is implemented by Python in the following way.
>>> "bark" * 5
'barkbarkbarkbarkbark'
Along with arithmetic expressions, another type of expression that is important are Boolean expressions. These are expressions that evaluate to Boolean values, True or False. For example, statements like "5 is greater than 7" evaluate to either true or false (this statement is false by the way).
| Python | Math | English |
|---|---|---|
x < y | $x \lt y$ | $x$ is less than $y$ |
x > y | $x \gt y$ | $x$ is greater than $y$ |
x <= y | $x \leq y$ | $x$ is less than or equal to $y$ $x$ is at most $y$ |
x >= y | $x \geq y$ | $x$ is greater than or equal to $y$ $x$ is at least $y$ |
x == y | $x = y$ | $x$ is equal to $y$ |
x != y | $x \neq y$ | $x$ is not equal to $y$ |
We can also call these relational operators comparisons, since they compare values $x$ and $y$. It's important to note that the equality operator is ==, not =, since = is already used for assignment. This is a very common source of errors, no matter how experienced of a programmer you turn out to be, now and in the future.
These have a very well-established meaning for numbers, but it turns out that these operators can be used on strings as well. It's not hard to see how == and != can easily be applied on strings, but it may be surprising to learn that the other operators can be applied to strings too.
For instance, this can be surprising:
>>> "two" > "one"
True
How did Python figure out that $2 \gt 1$? Well, actually,
>>> "two" > "three"
True
This does not actually seem right. So what's going on? Whenever we can use a relation like $\gt$, it means that the objects we're working with have some sort of order. Informally, we can think of strings as being ordered alphabetically (though that is not really the entire story, we'll leave it at that for now...).
We can use these basic boolean expressions (also called predicates) and combine them into more complicated expressions by using Boolean connectives. Suppose we have two Boolean expressions p and q. The Boolean connectives are defined by
p and q is True if both p and q evaluate to True, and False otherwise.
p or q is True if at least one of p or q evaluates to True, and False otherwise. Note that this definition of or is a bit different from many casual uses in English, where or tends to mean exactly one of the choices. For instance, if you are asked whether you would like tea or coffee, you would get weird looks if you say both.
not p is False if p evaluates to True, and it is True otherwise.
The following table summarizes each of the cases above.
p | q | p and q | p or q | not p
|
|---|---|---|---|---|
True | True | True | True | False
|
True | False | False | True | |
False | True | False | True | True
|
False | False | False | False |
Like arithmetic expressions, there is an order of operations for boolean connectives:
not),
and),
or).