ICS 32A Fall 2023
Notes and Examples: Python Basics


Launching a Python shell

The simplest way to interact with Python is to use something called the Python shell. If you followed the installation instructions in Project 0B, then you'll have two ways available to start up a Python shell:

Either way, having launched a Python shell, you'll see text similar to this appear:


    Python 3.11.5 (tags/v3.11.5:cce6ba9, Aug 24 2023, 14:38:34) [MSC v.1936 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license()" for more information.
>>>

The first part of that text identifies the current version of Python you're running — what you see above is what you'd see if you installed the 64-bit version 3.11.5 on Windows, but you'll see something slightly different if you're running something else — while the >>> is actually the most important thing. That's called the shell prompt.

The way you interact with the Python shell is to type an expression at the shell prompt, then hit the Enter or Return key. Python evaluates the expression, determines its value, and then displays that value. Afterward, you'll see another shell prompt, allowing you to type another expression, and so on. This continues until you quit the Python shell. (Generally, this style of interaction is often called a read-evaluate-print loop or REPL. As you continue studying computing, you'll see that lots of programming languages offer this style of interaction, though not all of them do.)

As a first example, you might start with the following interaction with the Python shell:


>>> 4
    4
>>> 'Hello'
    'Hello'

The rule we're seeing in action above is that some kinds of expressions — the simplest kinds, for the most part — evaluate to themselves. If all you type is a number or some text surrounded by single-quotes, you get back exactly what you typed. But those are the least interesting examples, so let's try some other things.


>>> 2 + 2
    4
>>> 19 - 6
    13
>>> 3.5 + 2.25
    5.75

We see that numbers can participate in arithmetic calculations like addition and subtraction, and that we can specify numbers that have a fractional part. In these examples, + and - are what we call operators. Multiplication and division are supported, too, with the * and / operators used to represent them:


>>> 5 * 4
    20
>>> 12 / 4
    3.0

If you've got a keen eye for detail, you'll notice that the result of dividing 12 by 4 was given as 3.0 instead of 3. There's a somewhat longer story there, which we'll return to in a little while, but the short version is that the kind of division we did — using the / operator — always returns the kind of numbers that include a fractional part, even if that fractional part is .0. (If you didn't notice, don't feel bad, but do work on getting yourself in the habit of noticing the small details. Those details are usually trying to tell you something, or offering you a distinction that you can learn something from.)

As it turns out, not every expression can be evaluated, so you'll sometimes see an error message instead of a value. Some things you'll type simply aren't legal according to Python's syntax rules, in which case they aren't evaluated at all.


>>> Boo is sleeping next to me
    SyntaxError: invalid syntax

Some things are syntactically legal, but fail during evaluation for some other reason. Division by zero is one example, and we'll see many more.


>>> 5 / 0
    Traceback (most recent call last):
      File "<pyshell#8>", line 1, in <module>
        5 / 0
    ZeroDivisionError: division by zero

When you see a traceback, this is Python's way of saying that it encountered a failure while evaluating your expression. Tracebacks contain a fair amount of useful information:

When you're done with the Python shell and you want to exit, there are a couple of ways to do it.


Types, objects, numbers, and arithmetic operators

We saw previously that numbers are displayed in two different ways: with or without a fractional part. Perhaps counterintuitively, we even saw a number displayed as 3.0, even though it could just as easily have been displayed as 3. Meanwhile, we also saw the number 4 displayed without the fractional part. There's a meaningful distinction here, and understanding what's at the root of that distinction will teach us something broadly useful about Python.

Every expression is evaluated and that evaluation (if successful) yields a value. A little more technically, we call each of these values an object. Notably, every object will always have a type. A type is a means of classifying that object, governing what operations are legal and illegal to perform on it, as well as what happens when you perform legal operations. One thing an object's type determines is how the Python shell will display it; each type has what's called a representation, which is a way of determining what objects of that type should look like when displayed in the Python shell.

Returning, then, to the question of why we sometimes saw numbers show up with fractional parts and other times did not, we might infer that the reason is because the numbers had different types. Let's test our theory. Python includes a way to ask that question; by surrounding an expression with parentheses and writing the word type before it, we can find out what the type of value returned by that expression is.


>>> type(4)
    <class 'int'>
>>> type(11.875)
    <class 'float'>

One thing to note is that we did receive a different answer when we asked for the type of 4 as opposed to the type of 11.875. That tells us that these values do, indeed, have different types.

Another thing to note is that we asked for each value's type, but what we got back was something called a class instead. You'll find that the terms "class" and "type" are used somewhat interchangeably in Python. The distinction between them is mostly a historical anomaly — differences that were evident in much older versions of Python, but that have long since been unified — so you can think of these terms as meaning the same thing. If you don't believe me, look what happens when we ask for the type of a value's type.


>>> type(type(4))
    <class 'type'>

The reason this works is because even types are objects; their type is called type.

Ints and floats

An int (or an integer) is a number without a fractional part, such as 15 or -12345. Python's integers can be either positive, negative, or zero, and there is no limit on how large or how small they might be.


>>> 123456789012345678901234567890
    123456789012345678901234567890
>>> type(123456789012345678901234567890)
    <class 'int'>

A float is what's called a floating-point number, which is a number with a fractional part, such as 5.75 or -4.0. Unlike integers, there is a practical limit on the size of a float, though it's a bit more difficult to describe and it can vary from one installation of Python to another. For all practical purposes, you can think of floats as capable of being almost endlessly large or endlessly small, but not endlessly precise. If you ever learned about scientific notation for numbers in a science course, such as 6.02 x 1023, that's a pretty good way of thinking about what a floating-point number is: a value derived from a mantissa (6.02) and an exponent (23). The difference is that the mantissa is stored as a binary number (base 2, rather than base 10) and the exponent is a power of 2 instead of 10. There is a limit on the number of digits available in the mantissa and in the exponent, which is why floats aren't endlessly precise. There is much more to be said about floating-point numbers, though we'll defer the rest of that discussion for another day.

There are several arithmetic operators that you can use on integer and float values, which are demonstrated below.


>>> 5 + 4
    9
>>> 2.5 + 3.75
    6.25
>>> 20 - 9
    11
>>> 4.5 - 3.125
    1.375
>>> 4 * 9
    36
>>> 2.25 * 3.0
    6.75
>>> 15 / 3
    5.0
>>> 15.0 / 3.0
    5.0
>>> 15 // 3
    5
>>> 15.0 // 3.0
    5.0
>>> 2 ** 10
    1024
>>> 2.1 ** 3.3
    11.569741950241465

There are a few things to note from these examples:

Division vs. integer division

Python supports two kinds of division:

Let's explore the two kinds of division more carefully, taking note of the types of values we get back from it, in addition to the values themselves. When used on two integers, the two division operators work like this.


>>> 12 / 4
    3.0
>>> type(12 / 4)
    <class 'float'>
>>> 11 / 4
    2.75
>>> type(11 / 4)
    <class 'float'>
>>> 12 // 4
    3
>>> type(12 // 4)
    <class 'int'>
>>> 11 // 4
    2
>>> type(11 // 4)
    <class 'int'>

We see that, when used on two integers, the division operator / always returns a float; the integer division operator // always returns an int. Let's see if that's true when we use them on two floats instead.


>>> 12.0 / 4.0
    3.0
>>> type(12.0 / 4.0)
    <class 'float'>
>>> 11.0 / 4.0
    2.75
>>> type(11.0 / 4.0)
    <class 'float'>
>>> 12.0 // 4.0
    3.0
>>> type(12.0 // 4.0)
    <class 'float'>
>>> 11.0 // 4.0
    2.0
>>> type(11.0 // 4.0)
    <class 'float'>

In this case, we get a float result using either of the division operators, the difference being that integer division // gives us an integral result (i.e., a result with the fractional part discarded and replaced with .0 instead). The only other interesting question is what happens when we use floats with fractional parts other than .0.


>>> 3.25 / 0.5
    6.5
>>> type(3.25 / 0.5)
    <class 'float'>
>>> 3.25 // 0.5
    6.0
>>> type(3.25 // 0.5)
    <class 'float'>

Nothing there disproves our theory. The rules are:

Remainders

There is one additional wrinkle when it comes to division. Not only is there a way to perform integer division, but there is also a way to obtain the remainder of that division, using the remainder (also called mod or modulo) operator, which is denoted in Python by %. What this returns is the remainder left over after performing an integer division, similar to how I originally learned about mathematical division a very long time ago (before I had been taught about fractions or decimals).


>>> 6 % 3
    0
>>> 7 % 3
    1
>>> 8 % 3
    2
>>> 9 % 3
    0
>>> 10 % 3
    1

Since 6 is divided evenly by 3, there is no remainder, so the result is 0. 7 does not divide evenly by 3; we can group 3 elements twice and then have 1 left over, so the result is 1.

At first blush, this seems like a strange operator to include in a programming language. Why would we ever want the remainder of a division? However, remainders are surprisingly handy, due to some useful characteristics they have.

Note that it's possible to find remainders using negative integers on either or both sides, and it's also possible to do it using floats, though neither of those techniques has nearly as much practical use as what we've already seen, so we'll leave those as curiosities for now and move on.

Mixed-mode arithmetic

Python also supports what is often called mixed-mode arithmetic, which is a fancy way of saying that you can perform arithmetic operations on two operands where one is an int and the other is a float. In that case, the int operand is first converted to an equivalent float (e.g., 14 becomes 14.0), then the operation is performed using the usual rules we've already seen for operations involving two floats.


>>> 5 * 3.5
    17.5
>>> 12.5 // 3
    4.0

Operator precedence, associativity, and parenthesization

The arithmetic operators (and all of the others, too) each have a precedence associated with them, with some having a higher precedence than others. In an expression with multiple operators, the operators with higher precedence are evaluated before those with lower precedence. As is typical in algebra, multiplication and division have a higher precedence than addition and subtraction.


>>> 3 + 4 * 5
    23

Operators at the same level of precedence are evaluated according to their associativity. Most operators associate from left-to-right when precedences are the same, meaning that the operator on the left is evaluated before the operator on the right. That may seem like an irrelevant detail, but it actually turns out to be important. Subtraction associates left-to-right, which makes the example below work the way it's shown below.


>>> 5 - 3 - 2
    0

If subtraction associated right-to-left instead, the answer would be different: First, 2 would be subtracted from 3 (giving 1), which would then be subtracted from 5 (giving 4).

While most operators associate left-to-right, some don't. One example is exponentiation, which associates right-to-left — as it does in mathematics, when we write things like 432.


>>> 4 ** 3 ** 2
    262144

In this case, 3 ** 2 is evaluated first (giving 9), then 4 ** 9 is evaluated (giving 262144).

As is also typical in algebra, parentheses can be used to override the normal precedence of operators. In an expression, operators within a set of parentheses are evaluated before operators outside of it.


>>> (3 + 4) * 5
    35
>>> 4 * ((5 + 6) / 2)
    22.0

Of course, there's only so far we can get in a programming language by evaluating arithmetic expressions, so let's expand our horizons a bit.


Variables

A variable in Python provides a way to give a name to some value, and to retrieve that value (using its name) again later. Introducing a new variable simply requires that we use assignment to store a value into a variable that we hadn't used previously. Subsequently, if we use that variable in an expression, it will evaluate to the value we stored in it previously.


>>> boo = 11
>>> boo
    11
>>> boo * 4
    44

Statements

Until now, every time we've typed something into the Python shell, it's been evaluated and its value has been displayed. In our use of assignment, however, things were different. We typed boo = 11 into the shell, but we didn't see any result at all. Instead, we were simply shown another prompt, expecting us to type something else.

Assignment in Python is an example of a statement. Statements differ from expressions in that they do not have a value; they instead achieve some other kind of effect. In this case, the effect we got (by using assignment) was to store a value into a variable. There's no value returned; there's simply a change made behind the scenes (the variable boo now has the value 11 stored in it). We'll see a number of other kinds of statements, which will have different effects. But that distinction will always remain: Expressions are evaluated and return a value, while statements do not return a value, but instead have some other kind of effect.

Values have types, but variables do not

Depending on which programming languages you've seen previously, you might be wondering why it wasn't necessary to introduce the variable into the program in some way before assigning a value to it. For example, in Java, you introduce a new variable by declaring it, after which you can assign it a value.


int boo;

. . .

boo = 11;

The primary reason for variable declaration in a programming language is to associate a variable with a type. In Java, once you've declared that the variable boo has the type int, you'll only ever be allowed to store an integer value in boo. The limitation is enforced so strongly that a Java program can't even run if there's an attempt to store something other than an integer in the variable boo; a Java compiler will reject the program with an error message before it can run.

In Python, however, variables don't have a type. Variables can store any value you'd like at any time. The same variable can even store values of different types throughout its lifetime — though you might reasonably question the wisdom of such an approach, it is legal, even if the types are wildly different from one another.


>>> boo = 11
>>> boo
    11
>>> type(boo)
    <class 'int'>
>>> boo = 4.25
>>> boo
>>> 4.25
>>> type(boo)
    <class 'float'>

Because it's possible to say type(boo), you might be thinking that the variable boo has a type, but it really doesn't. What we're really doing when we say type(boo) is this:

The key thing to remember is that variables in Python do not have a type, but they each store a value, and the value they store does have a type. That may seem like a distinction without a difference, but will actually turn out to be important as we move forward.

This means that whether it's legal to use a variable in a Python expression is largely a matter of what type of value it stores. For example, if we attempted to evaluate the expression boo + 8, that might or might not be legal; it depends on what kind of value is stored in boo. If it's a value to which we can add an integer, it will be legal; if not, it won't. (This is why, in practice, we'll want to consider the types of values we plan to store in each of our variables, even though it's not checked by Python until the program runs. The name of the game, in any programming language, is to write a program that works correctly, and that requires the requisite level of care, no matter what language you're writing in.)

Naming conventions

When we write Python programs, we'll follow certain naming conventions that are typically used by Python programmers. The word "conventions" means something less strict than "rules" or "laws"; a naming convention is not an absolute (i.e., you can technically violate it and still be writing a Python program that can run successfully). But when everyone who writes Python programs names similar kinds of things similarly — following the same norms for capitalization, spacing, and so on — then everyone benefits; seeing names written in a certain way provides a valuable clue about what they mean, which can make a program easier to read and understand.

For that reason, we'll follow the usual naming conventions for Python in this course. Having learned a language other than Python previously, you may find that you're used to something different, so what you see here may seem jarring. But part of learning a new programming language is learning the culture around that language, the shared ideas and techniques that help people writing in that language collaborate and understand each other more easily; naming conventions are part of that culture.

In Python, variables are named using only lowercase letters, digits, and underscores. It turns out that the digits can't come first — that's a rule in Python, actually, which makes a name like 123boo for a variable illegal — but, otherwise, anything goes. The underscores are used to separate words, for names that consist of more than one word. All of these are valid variable names in Python, according to the naming convention:


boo
field3
instructor_name
student_id_number


Relational operators

In addition to the arithmetic operators we've already seen, Python also includes relational operators (also called comparison operators) that allow you to determine how two values compare to one another — whether they are equal, not equal, less than, less than or equal, greater than, or greater than or equal to one another. These work more or less the way you would expect; some examples follow.


>>> 3 == 3
    True
>>> 4 == 3
    False
>>> 3 != 3
    False
>>> 4 != 3
    True
>>> 2 < 3
    True
>>> 3 < 3
    False
>>> 4 < 3
    False
>>> 2 <= 3
    True
>>> 3 <= 3
    True
>>> 4 <= 3
    False
>>> 2 > 3
    False
>>> 3 > 3
    False
>>> 4 > 3
    True
>>> 2 >= 3
    False
>>> 3 >= 3
    True
>>> 4 >= 3
    True

Note, too, that it is possible to do all of these things with floats, and also to mix integers and floats, with predictable results.


>>> 4.0 == 4.0
    True
>>> 3 <= 4.0
    True
>>> 6.5 > 8
    False

Chaining relational operators in the same expression

One feature you might not expect, depending on which programming languages you've used previously, is that you can chain more than one relational operator together in the same expression.


>>> 2 < 4 < 8
    True
>>> 14 >= 8 < 3
    False

The key to understanding these expressions is to realize that it's actually a shorthand for something else. The expression 2 < 4 < 8 is really a way to ask "Is it true that 2 is less than 4 and that 4 is less than 8?" If both parts are true, the whole expression will evaluate to True; otherwise, it will be False. That's why the second of the examples above is False: 14 is greater than or equal to 8, but 8 is not less than 3.

The main reason you'd want to chain relational operators together is the same way we often do it in mathematics: It's a handy way to establish that a value lies between two others, or that three or more values are all equal. This is especially useful when we're using variables.


>>> x = 3
>>> 0 < x < 5
    True
>>> y = 4
>>> z = 5
>>> x == y == z
    False

The values True and False

Another thing to note is that the values we got when we evaluated expressions involving relational operators weren't integers or floats; instead, we always got back either the value True or the value False. This might rightly make you wonder what these values are; there's no better way to find out than to ask the Python shell.


>>> type(2 > 3)
    <class 'bool'>

So, what's a bool? That's the next part of our story.


Bools

The bool (or Boolean) type is one that you can think of as comprising two values: True and False. (Technically, a bool is a kind of integer in Python, though we will likely never rely on that detail. When we want a "true/false" value, we'll use bool; when we want a number, we'll use a number. That you can do things like arithmetic using bools is mainly a curiosity; it doesn't have a lot of practical use, so we'll avoid it.)

In fact, True and False are legal Python syntax on their own, literally meaning these two bool values.


>>> True
    True
>>> type(True)
    <class 'bool'>
>>> False
    False
>>> type(False)
    <class 'bool'>

You can compare two bools use the equality operators and get the result you'd expect.


>>> True == True
    True
>>> True == False
    False
>>> True != True
    False
>>> True != False
    True

More importantly, you can use logical operators with bools, which also work in ways you would expect. The not operator negates its bool operand, so True becomes False and vice versa.


>>> not True
    False
>>> not False
    True

The and and or operators combine two or more bools, according to the rules you might expect: and returns True if all of its operands are True, while or returns True if any of its operands are True.


>>> True and True
    True
>>> True and False
    False
>>> False and True
    False
>>> False and False
    False
>>> True and False and True and True
    False
>>> True or True
    True
>>> True or False
    True
>>> False or True
    True
>>> False or False
    False
>>> False or False or True or False
    True
>>> True or False or True or False
    True

Of course, in practice, we won't just be using and, or, and not to combine literal True and False values. Instead, we'll be using them to combine the results of other expressions that evaluate to these values.


>>> a = 12
>>> b = 9
>>> a < 15 and b > 0
True
>>> a < 10 or b == 10
False

The primary use we'll find for bool values is in truth testing, which will allow us to write Python code that behaves differently depending on the result of that testing. To get into that topic in detail, though, we'll need to know a little more Python first, but we'll return to this topic shortly.


Writing Python scripts

So far, we've spent a lot of time interacting with the Python shell. The Python shell is obviously handy because of its interactivity, but it's a tool for people who write programs, not for people who use them. So, how do we write Python programs that can be used by non-programmers? Or save programs so we can run them again and again, without re-typing them into the shell? The answer is to write Python scripts.

Naming a Python script

A Python script isn't really all that special; it's a file that contains text, the same kind of text (Python code) that you could type into the Python shell. What makes it a Python script is mainly a matter of naming it appropriately. Similarly to variables, there is a naming convention that is typical for Python scripts, which we'll follow:

The second part of the convention is there mainly so that we can write Python programs that consist of more than one script; it facilitates the ability for one Python script to "import" another, a topic we'll return to soon. By naming scripts similarly to how we name variables, we're sure that we'll be able to use the name of one script within another.

So, by these rules, these would all be allowable names for Python scripts:


project0b.py
my_program.py
rock_paper_scissors.py

while these would not:


Project #0B.py
MyProgram.py
rock_paper_scissors.py.txt

(As it turns out, the latter three are not technically illegal, but, as we'll see, they're a really bad idea; we'll stick with the naming convention above.)

Our first Python script

Let's write our first Python script. In IDLE, select the File menu, then New File. A new, empty window will appear, while the IDLE window containing the Python shell will remain in the background. In your new window, type the following text. (Note, too, that you could type this same text into a Python shell; Python code is Python code.)


first = 10
last = 30
last - first

After typing that text, but in that same window, select the File menu, then Save. You'll be asked to choose a location for the file and a name; give it the name example1.py, our first example.

Now that we've saved the file, we can run our Python script and see what it does. From the Run menu, select Run Module. You'll notice that the IDLE window containing the Python shell will return to the foreground, with text similar to this now appearing in the window:


    ============== RESTART: D:/Examples/32A/example1.py ==============
>>>

All we saw was that the shell was restarted — that's why it says RESTART — which means that any previous state (e.g., variables we had previously assigned in the Python shell) will have been wiped clean. Then we see the usual >>> prompt, and the Python shell is waiting for us to type an expression.

But where's our output? The third line of our program said last - first, so we might have expected to see the value 20 appear, just like it would have appeared if we had typed those same three lines of code into a Python shell. But it didn't. Why is that? Did our script even run? Let's check, by typing some expressions into the Python shell.


>>> first
    10
>>> last
    30

It sure looks like it, because the variables first and last have the values assigned to them in our script. So, why didn't we see the result of last - first?

The answer to that question is that running a script isn't the same as typing expressions into a Python shell. When you type an expression like last - first into a Python shell, it's evaluated and its value is displayed. But when an expression is evaluated within a Python script, we see nothing. It happens, but if nothing is done with its value, that value is simply discarded.

So, how can we write a Python script that generates output we can see?


Printing output

Python includes a set of built-in functions, which are always available to us and provide some basic capabilities that are required in Python programs. We've seen one of those built-in functions, called type(), already. Another of those built-in functions is called print(), whose job is to display output; if we use that from within a Python script, we'll see the output it displays when we run the script.

Let's modify our first example script, so it uses print() on the last line.


first = 10
last = 30
print(last - first)

Note that, just like the type() function, we call the print() function by specifying its name, followed by a pair of parentheses. Between the parentheses are what are called arguments. In this case, we're passing only one of them — the result of evaluating the expression last - first — and asking the function to print that value.

Let's see what happens when we run this new version of our script.


    ============== RESTART: D:/Examples/32A/example1.py ==============
    20
>>>

Now we're getting somewhere! The Python shell was restarted, our script was executed, its output (20) was printed, and we then see the >>> prompt, like before. (When we run a Python script within IDLE, we'll always get a shell prompt at the end, which will allow us to explore the aftermath of our script's execution.)

I should point out that you can use the print() function within the Python shell, as well, but it has more limited use there, since its goal — to make output visible — is already met by the Python shell's feature of displaying the value of whatever expression we evaluate.


>>> x = 15
>>> x
    15
>>> print(x)
    15

A few more details about print()

Before we move forward, there are a few things worth highlighting about the print() function.

You can pass more than one argument to the print() function. (The way you pass multiple arguments to a function in Python is to separate the arguments with commas.) When you do that, the arguments' values are printed in the order you passed them to the function, with a space separating them.


>>> print(10, 20)
    10 20
>>> print(4.5, 30, -2.75)
    4.5 30 -2.75

If you don't want the arguments' values to be separated by spaces when printed, there's a way to control that, as well. Some Python functions can accept what are called keyword arguments, which is to say arguments that are explicitly named. The main rules to be followed are that the keyword arguments must appear after all of the "normal", non-keyworded ones, and that the function must recognize the keyword argument(s) you pass to it. In the case of print(), it supports the keyword argument sep — short for separator — which allows you to specify what should be printed in between each argument.


>>> print(10, 20, sep = 'e')
    10e20

Ordinarily, when you call print(), it prints the values of whatever arguments you pass to it, then prints a newline character, meaning that the next output to be printed will appear at the beginning of the next line. However, you can control that behavior, as well; the keyword argument end allows you to specify what should be printed after the values of the arguments.


>>> print(10, 20, sep = 'e', end = 'X')
    10e20X

Putting all of that together, suppose you have a Python script with the following code in it.


x = 5
y = 7.5
z = 10
print(x, y, sep = 'q', end = '')
print(z)
print(x + y + z)

If you executed that script, you would see this output:


5q7.510
22.5


Reading input

Thus far, we've been able to write Python code that displays output, but most interesting programs also need to read some kind of input. There are actually a lot of ways for a Python program to receive input:

We'll see all of these techniques during our travels this quarter, but we'll begin with the simplest of these: reading input from the keyboard via the Python shell.

The built-in function input()

Python provides a built-in function called input(), whose job is to wait for the user to type a line of input, then return whatever the user typed. The function won't finish until the user has pressed the Enter or Return key to signify that they've finished typing.

Let's try it from the Python shell first.


>>> x = input()

At this point, the cursor will appear at the beginning of the next line, waiting for the user to type something. We won't see the next >>> prompt, because the input() function hasn't finished executing yet. So, for example, if we typed 35 and hit the Enter or Return key, then input() could finish, the assignment to the variable x could occur, and we'd see the next prompt.


>>> x = input()
    ​35​
>>>

Since x = input() is an assignment statement, it has no value, so nothing is printed. (Note that 35 is something we typed, not something that the Python shell displayed.)

Now that the assignment is complete, we might expect x to have the value 35. Let's see if we're right about that.


>>> x
    '35'

The single quotes surrounding the 35 aren't an accident; they mean something. The value in x isn't an integer; it's something else. And if we want to know what it is, a good first step is to ask the Python shell to tell us.


>>> type(x)
    <class 'str'>

But if we typed the digits 35, why didn't input() give us back an integer instead? Before we answer that question, let's make sure we understand what str means.


Strings

A string is a sequence of text, made up of zero or more characters. The str type in Python is used to represent strings. (Notably, there is no separate type for characters; characters are represented as strings containing one character instead.) Different strings can have different lengths, and there is no official limit on the number of characters that can be in a string in Python — though there is a practical one, in the sense that your computer has a finite amount of memory available.

A string literal in Python is a way to write a string whose characters are specified directly in your program. There are a few ways to write string literals in Python, the most common of which are:

Note, though, that these are just different syntaxes for the same result; either way, what you get back is a string.


>>> 'Hello'
    'Hello'
>>> type('Hello')
    <class 'str'>
>>> "Hello"
    'Hello'
>>> type("Hello")
    <class 'str'>

The Python shell prefers displaying strings by surrounding them with single-quotes, so you'll notice that when we evaluated "Hello", we saw that the Python shell displayed it as 'Hello' instead. We'll prefer the same in the code that we write in this course; we'll use single-quotes, except when there's a good reason not to.

You might wonder why Python supports both syntaxes in the first place. The reason has mostly to do with being able to easily write string literals that themselves have quotes inside of them.


>>> 'His name is "Boo"'
    'His name is "Boo"'
>>> "He's perfect"
    "He's perfect"

Notice that the Python shell will display strings surrounded with double-quotes when they contain a single-quote (and don't also contain a double-quote).

Escape sequences

Of course, there are some syntax limitations here.

How we solve problems like these in Python is similar to how most programming languages allow us to solve them: by providing escape sequences. If the backslash character \ appears in a string literal, you've potentially started an escape sequence, which is to say that you've "escaped" from the normal rules of Python syntax temporarily. If the character(s) that follow the backslash are recognized as escape characters, Python will change the entire escape sequence — the backslash, plus the character(s) that follow — into something else that might otherwise be difficult or impossible to write in a string literal.

For example, single- and double-quote characters can be escaped this way.


>>> 'It\'s a beautiful day'
    "It's a beautiful day"
>>> "I said \"Isn't it?\""
    'I said "Isn\'t it?"'

(Notice again that the Python shell preferred to display the string surrounded by single-quotes in the second case. It always uses single-quotes, unless the string contains single-quotes and doesn't contain double-quotes.)

Another commonly-used escape sequence is \n, which is replaced by a newline character.


>>> 'Hello\nthere'
    'Hello\nthere'
>>> print('Hello\nthere')
    Hello
    there

There are a couple of interesting things going on in the example above, which we should take note of. When the Python shell displays a string value that results from evaluating an expression, it uses the string's representation, which is done using Python syntax; what we see is itself a string literal. This is why evaluating 'Hello\nthere' led to the result 'Hello\nthere' being displayed. On the other hand, when Python prints a string, it displays only the characters in that string; in that case, the newline character actually manifests itself as a newline (i.e., going to the beginning of the next line after printing Hello but before printing there).

A similar technique allows us to include a backslash character into a string literal. The escape sequence \\ becomes a single backslash.


>>> 'a\\b'
    'a\\b'
>>> print('a\\b')
    a\b

There are a number of other escape sequences supported by Python, and we'll no doubt see some more of them as we move forward. Note, though, that not every character following a backslash forms an escape sequence. If we follow a backslash with something that doesn't form an escape sequence, we get what we wrote — the backslash is included in the string literal.


>>> 'a\gb'
    'a\\gb'
>>> print('a\gb')
    a\gb

Asking for the length of a string

The built-in function len() in Python provides a way to ask a value for its length. Not all types of values have lengths, but strings certainly do: The length of a string is the number of characters in it.


>>> len('Boo')
    3
>>> len('Hello there')
    11
>>> len('a\\b')
    3

Note, from the last example, that what is being counted are the characters actually stored in the string. Escape sequences are translated to something else before they're stored, which means that len('a\\b') is 3, because the escape sequence \\ becomes a single backslash.

Not all kinds of values have lengths. Numbers, for example, do not; asking a number for its length will give you an error message.


>>> len(5)
    Traceback (most recent call last):
      File "<pyshell#33>", line 1, in <module>
        len(5)
    TypeError: object of type 'int' has no len()

Concatenation and multiplication

Strings can be concatenated by using the + operator. Concatenation simply means to combine two strings into a single one, such that the text from the second of the two strings appears immediately after the text from the first.


>>> 'abc' + 'def'
    'abcdef'

They can also be multiplied by using the * operator. When you multiply a string, you don't multiply it by another string; you multiply it by an integer, with the result being the original string appearing repeatedly the specified number of times.


>>> 'boo' * 3
    'boobooboo'

Not all arithmetic operators are supported by strings, however. There is no notion of subtraction or division supported; attempting to subtract or divide strings is an error.


>>> 'abc' - 'bc'
    Traceback (most recent call last):
      File "<pyshell#36>", line 1, in <module>
        'abc' - 'bc'
    TypeError: unsupported operand type(s) for -: 'str' and 'str'
>>> 'hellohello' / 2
    Traceback (most recent call last):
      File "<pyshell#37>", line 1, in <module>
        'hellohello' / 2
    TypeError: unsupported operand type(s) for /: 'str' and 'int'

Relational operators on strings

Relational operators are supported by strings, which means that you can compare them for equality, inequality, or ordering. Two strings are equal if they contain the same sequence of characters (i.e., the same characters specifed in the same order).


>>> 'Hello' == 'Hello'
    True
>>> 'Hello' != 'Hello'
    False
>>> 'Hello' == 'Hello '
    False
>>> 'Hello' != 'Hello '
    True
>>> 'Hello' == 'hello'
    False
>>> 'Hello' != 'hello'
    True

Comparisons for ordering, using operators such as < or >=, are done using a technique called lexicographical ordering, which is a somewhat longer story that we'll return to a bit later in the course.

Calling methods on a string

We've seen before that the values that Python programs operate on are said to be objects. If you've previously learned a language such as Java, C++, or C#, you'll be familiar with the term, so you may have certain expectations about how they must work in Python, too — though, as usual, previous knowledge can also be slightly misleading, so be careful how many assumptions you make about the similarity of Python to languages that you already know.

Many kinds of objects in Python support what are called methods, which are similar to functions, but that you instead ask an object to perform on your behalf. Not nearly every function you write in Python will be a method, but methods do exist, and we'll need to know how to call them — and, eventually, how to write them, though that's a topic for another day.

When you want to call a method on an object — which is to say that you want to ask the object to do a job for you — you do so by following that object with a dot, then the name of the method, then arguments surrounded by parentheses (similar to when you call a function). Strings support a number of methods, so let's take a look at how some of them work.


>>> description = '  Boo is happy today!  '
>>> description.strip()
    'Boo is happy today!'

The strip method returns the string it's called on, but with any spaces at the beginning and the end removed. Note that description will not have changed; what we're getting back is a new string that's similar to the existing one. (That's true of all of the methods that the str type provides; they don't modify the strings they're called on.) You can think of these methods as asking a string a question: "What would you look like if you had no spaces at the beginning and the end?"


>>> description.upper()
    '  BOO IS HAPPY TODAY!  '
>>> description.isupper()
    False

As you might imagine, the upper method returns a string where any lowercase letters have been replaced by uppercase letters instead. isupper instead answers the question of whether every letter is uppercase.


>>> 'Boo'.upper()
    'BOO'
>>> 'Boo'.upper().isupper()
    True

One thing to realize is that you can call a method on a string, even if that string isn't stored in a variable. That allows you not only to call a method on a string literal, but also to chain together two or more string methods on the same line of code.


>>> description.startswith('B')
    False
>>> description.startswith(' ')
    True
>>> description.strip().startswith('B')
    True

The startswith method returns True if called on a string that starts with the characters in the string you pass as an argument to it.

There are lots more that you can call on strings, and we'll see some of them later in the course, but let's move forward to other topics for now.

The input() function always returns a string

Now that we know what strings are, we can return to a question we had left open previously. We did the following in the Python shell, but got back a somewhat curious result.


>>> x = input()
    ​35​
>>> x
    '35'
>>> type(x)
    <class 'str'>

The reason the value in x is a string and not an integer is simple: Users can type any text they want in response to a call to the input() function, so the input() function always returns a string, since only a string can safely represent anything the user could have typed.

Of course, the problem arises when what we really want is an integer, because we subsequently want to use the value in ways that only an integer supports (e.g., to perform arithmetic on it).


>>> num = input('Enter a number: ')
    ​35​
>>> num + 2
    Traceback (most recent call last):
      File "<pyshell#12>", line 1, in <module>
        num + 2
    TypeError: must be str, not int

Conversions between strings and other built-in types

It is possible in Python to convert strings to many of the other built-in types. Each of the built-in types has a constructor, which is a function that can create a value of that type. Each of them supports slightly different options, but they all support the option of taking a string as an argument. These constructor functions will do nicely as a way of converting a string to an integer or a float.


>>> int('35')
    35
>>> type(int('35'))
    <class 'int'>
>>> float('10.5')
    10.5
>>> type(float('10.5'))
    <class 'float'>

These functions will also do nicely for helping us to detect that the string can't be converted, because it contains text other than digits. They'll generally strip spaces off of the beginning and the end of the string before conversion, but they won't allow the conversion if the string is non-numeric; in that case, an error message will appear.


>>> int('  15  ')
    15
>>> int('boo')
    Traceback (most recent call last):
      File "<pyshell#49>", line 1, in <module>
        int('boo')
    ValueError: invalid literal for int() with base 10: 'boo'

Conversions from string to bool are a little more problematic, because they don't behave the way you would expect; non-empty strings are considered True and empty strings are considered False.


>>> bool('True')
    True
>>> bool('False')
    True
>>> bool('123')
    True
>>> bool('0')
    True
>>> bool('')
    False

The str type has its own constructor, which can do this same kind of conversion in reverse. If you give it a value such as an integer, a float, or a bool, you'll get back a string containing something that looks like that value. (Technically, what you'll get is the same thing you get when you print a value, except it will be stored in a string instead of printed to the output.)


>>> str(10.5)
    '10.5'
>>> str(False)
    'False'

Reading numeric input

When we want to read numeric input, there are two tools that can be combined to solve the problem.

So, if we wanted to read, say, integer input, we could do the following. (Note, too, that the input() function can take a string argument, which is printed to the user as a sort of "prompt" to describe what they should do next.)


>>> age = int(input('Enter your age: '))
    Enter your age: ​42​
>>> age
    42
>>> type(age)
    <class 'int'>

If the user types something non-numeric, this will result in an error message, because the conversion being done by the int() constructor will fail. A little later this quarter, we'll learn how to react to situations like that and be able to handle them gracefully, but we'll allow the fragility for now.


Conditionality using the "if" statement

As in any programming language, Python provides a mechanism to achieve conditionality, so that you can write portions of your program that may or may not be executed, depending on the situation. (This turns out to be one of the very few critical features that a programming language must provide if it is to be generally useful.) In Python, conditionality is achieved using a statement called if.

Indention

The Python statements we've seen so far have been singular; they've stood alone. Even a statement with a complex expression in it, such as age = int(input('Enter your age: ')) is really just a single statement: an assignment into the variable age.

Some Python statements are what you might call compound statements, in the sense that they have other statements inside of them. The if statement is one such statement, because an if statement provides a way of saying "Under the following circumstances, run these statements; otherwise, don't."

Programming languages with compound statements need to have some way in their syntax to make clear which other statements belong within it. Some programming languages use symbols like semicolons, braces, brackets, or parentheses to play this role. Python, by way of contrast, uses the presence or absence of spacing; it is subject to indention rules instead.

An if statement provides a good example. Structurally, an if statement is made up of two things: a condition and a body. The condition is an expression that is evaluated as a way to decide whether the body should run; the body is a sequence of one or more statements that would be run if the condition says it should. So, for example, you could write the following Python script.


value = int(input('Enter a number: '))

if value > 0:
    print('That number is positive')

print('Goodbye!')

The if statement begins with the word if. The condition is the expression value > 0, which returns the value True if value is greater than 0, or the value False if not. Notice, too, the colon at the end of that line; that's a necessity, a way of telling Python that you're done writing the condition and are ready to begin the body. (In general, you'll find that the top line of compound statements in Python will end with a colon.)

The next line, where we're printing That number is positive, is indented. That's not just a stylistic matter in Python; that's a syntactic one. The line directly underneath the top line of our if statement needs to be indented at least one space. (It's not a bad idea to space things uniformly, for the sake of keeping a program cleanly readable. I'll always use four spaces for indention, which is also the default you'll see if you hit the Tab key in IDLE.) This tells Python that the statement on this line is part of the body of the if statement.

Where we say print('Goodbye'), we're back to the original level of indention, with the text on the line beginning in the same column as the word if. This, too, is syntactic, rather than just stylistic; it's this unindention that tells Python that the body of the if statement is complete, and that the line print('Goodbye') is not part of the body of the if statement.

Putting all of that together, here are a couple of separate runs of the Python script above.


    Enter a number: ​20​
    That number is positive
    Goodbye!

    Enter a number: ​-5​
    Goodbye!

Note that we'll see Goodbye! printed at the end, regardless of whether the number we enter is positive or not; that's because the last line, where Goodbye! is printed, is not part of the if statement — it's unconditional.

Truth testing and "truthiness"

An if statement makes its primary decision by performing something called a truth test. Its condition expression is evaluated, resulting in a value; that value is then tested to see if it is truthy or falsy. (Those sound like strange words, but that's actually how Python programmers describe this mechanism.) If the value is considered truthy, the body of the if statement is executed; if falsy, it's not.

The truthiness of bool values is not particularly surprising. True is truthy, while False is falsy. That's what made our previous example work the way it did: value > 0 returned the value True or False, and if that value was True, the body of the if statement was executed and That number is positive would be printed.

All other types of values in Python can be considered either truthy or falsy, as well; they don't have to be bools. Numbers, for example, are distinguished by whether or not they're zero — with zero being considered falsy, and non-zero being considered truthy. Strings are distinguished by whether or not they're empty — falsy if empty, truthy if they contain at least one character. And so on. So, for example, the following Python script would print You said something! whenever the user enters any input (as opposed to just hitting the Enter or Return key immediately).


what = input('Say something: ')

if what:
    print('You said something!')

In my experience, generally, I've found that relying on that behavior results in code that is more terse, but that is harder to read and understand, so I tend not to use this shortcut. If I want to write code that depends on the length of a string being positive, I'd rather say if len(s) > 0 than just saying if s. The name of the game is not to write as little code as possible. Programs are for people as much as they're for computers, so the objective is to write a program that people can best understand; say what you mean and mean what you say, so to speak. However, you should be aware that this is how Python works, because you will see people do things like this from time to time.

The "elif" and "else" clauses

An if statement can have additional clauses attached to it, which specify what might happen when the condition expression at the top of the if statement is falsy. These clauses are called elif and else. You can have as many of these as you'd like, but the else clause (if present) must be the last one.


num = input('Enter a number: ')

if num > 0:
    print('That number is positive')
elif num < 0:
    print('That number is negative')
else:
    print('That number is zero')

The meaning of this is what you might expect, with the clauses always considered in the order listed:

Ultimately, this script will print one of three things, depending on the user's input, but it will never print more than one of them.


Repetition using the "while" loop

Conditionality is one of the few features that a programming language absolutely must have to be broadly useful. Another is repetition, which is the ability to perform the same work repeatedly (e.g., a certain predetermined number of times, or until some condition is met). We see repetition all the time in software we use. A couple of examples follow, but there are countless more.

So, suffice it to say, programming languages need this feature, and Python is no exception. One way to achieve it in Python is to use a while loop.

The structure of a while loop is somewhat similar to that of the if statement that we saw previously, though don't let the similarity fool you; it solves a very different kind of problem.


num = int(input('Enter a number: '))

while num > 0:
    print(num)
    num -= 1

print('Goodbye!')

(Aside: The -= operator subtracts the number on the right from the value of the variable on the left. num -= 1, then, means to subtract 1 from the value of num. Many of the binary operators (i.e., operators that take two operands) in Python have variants like this, so you can expect to see things like x += 10, y *= 3, and so on, as we move forward.)

How a while loop is different from an if statement is simple: An if statement evaluates its condition once and then, optionally, executes its body once. A while loop does that same thing repeatedly.

Here's one example run of the script above.


    Enter a number: ​6​
    6
    5
    4
    3
    2
    1
    Goodbye!

The "break" statement

Within the body of a while loop, you can include a break statement, which, if reached, terminates the while loop immediately, with control flowing directly to the statement after the while loop. One place we'll see this is in the reading of user input, particularly when we want to check that input for validity — asking the user repeatedly until getting valid input — before accepting it and moving on.


while True:
    name = input('Enter your name: ')

    if name == '':
        print('Please enter a name; you entered nothing')
    else:
        break

print('Your name is ' + name)

The use of while True at the top of this loop isn't as cryptic as it looks. If a while loop continues running until its condition is falsy, one way to ensure that it doesn't ever end is by writing a condition that is always truthy. There are a lot of conditions in Python that are always truthy, but True is the clearest way to say it, so we'll stick with that.

Of course, we don't actually want the loop to run forever. But we won't know if a particular loop iteration will be the last one until it's done part of its work — namely, read a line of input and see if it's empty. By including a break statement in the loop, we have a way to "bail out" from it, but we preserve a simpler structure than the alternative of using the condition to get us out, which might look something like this instead.


name = ''

while name == '':
    name = input('Enter your name: ')

    if name == '':
        print('Please enter a name; you entered nothing')

print('Your name is ' + name)

In my estimation, this latter version is more convoluted. First, there is the assignment of the empty string into the variable name, which doesn't seem to have a purpose, but is strictly there to make the while loop's condition truthy the first time it runs. We then have the same expression name == '' in two places. All in all, this is a more obtuse way of achieving what we could more easily achieve with while True and break.

It's certainly not always the case that a while loop will be written with while True and break, but I've found this pattern for reading and validating user input to be a useful one.

Adding an "else" clause to a "while" loop

Like an if statement, a while loop can also have an else clause (though not an elif clause), though its meaning is a little bit different. An else clause on a while loop executes when the loop ends, but only if it ended normally (i.e., the condition was falsy); if, on the other hand, a break statement ended the loop, the else clause is skipped.

This technique is surprisingly useful as a simple way of differentiating when you have a loop that can end normally or might stop early; if you put code into the else clause, you can be sure it will only run if the loop ended normally. For example, the Python script below asks a user to type ten numbers, none of which can be zero, at which point it prints their sum. But if the user types 0, an error message is shown instead, with no sum displayed.


total = 0
count = 0

print('Please enter 10 non-zero numbers')

while count < 10:
    num = int(input('Next number: '))

    if num == 0:
        print('That number was zero; no sum for you!')
        break
    else:
        total += num
        count += 1
else:
    print('Thank you!  The sum of these numbers was', total)


Ranges

It is not at all uncommon that we want to count through a sequence of integers. If we were writing a Python program that was laying out text into a printable format such as PDF and we wanted the pages to be numbered consecutively, we'd need this ability. If we wanted to sum the integers from 1 through 10, inclusive, we'd need this ability. And so on. It's certainly true that we could solve this problem by storing an integer into a variable and then incrementing it with something like page_number += 1 repeatedly. But there's value in taking higher-level concepts, which occur often in programs, and representing them in a simple, coherent way. Taking a few lines of code — especially if they have to be sprinkled around in multiple places — and turning it into something that has a simple name is almost always a win; a program is almost always more readable that way, as long as the name is clearly chosen.

So, if we want to count through a sequence of integers, rather than doing that ourselves, we can rely on a built-in type of object called a range, which is capable of doing that job for us. By creating an object that represents our range, we can do things that would otherwise be difficult to do, such as passing the entire range as an argument to a function.

Creating a range is done using the built-in function range(), which is actually a constructor, in the sense that we've seen them already. How we know it's a constructor is by looking at the type of object it returns to us and realizing that its type is range.


>>> range(1, 10, 2)
    range(1, 10, 2)
>>> type(range(1, 10, 2))
    <class 'range'>

A range represents not one integer, but a sequence of potentially many of them. Ranges are described by three integers: a start, a stop, and a step.

When we pass three arguments to range(), as in our example range(1, 10, 2) above, then the three arguments are interpreted in that order: start, then stop, then step. So, this range starts at 1, stops at 10, with a step of 2, meaning that it actually represents the integers 1, 3, 5, 7, 9. One way to verify that we're on the right track here is to check its length, which we can do using the built-in len function.


>>> len(range(1, 10, 2))
    5

When you create a range, you don't have to specify all three of the values used to describe them. If you pass just two arguments to range(), what you're specifying is the start and the stop, but the step defaults to 1. So, for example, range(1, 10) would be a range starting at 1 and stopping at 10. That range includes the integers 1, 2, 3, 4, 5, 6, 7, 8, 9, so we would expect its length to be 9.


>>> range(1, 10)
    range(1, 10)
>>> len(range(1, 10))
    9

If we pass only one argument to range(), we're specifying only the stop value; the start defaults to 0 and the step defaults to 1. This means that range(6) would have start at 0, stop at 6, and have a step of 1, meaning it would contain the integers 0, 1, 2, 3, 4, 5.


>>> range(6)
    range(0, 6)
>>> len(range(6))
    6

We see, too, that the representation of a range when displayed in the Python shell always includes the start and the stop, but only includes the step if it's something other than 1.

You may be wondering how to get the integers out of the range, though. One way to do that is to use the indexing operator, which is denoted by an integer surrounded by brackets. Each integer in the range has an index associated with it; the first integer has the index 0, the second integer has the index 1, and so on. (It's not at all uncommon for "counting" to be done starting from 0 rather than 1 in computer science; Python is no exception.)


>>> r = range(1, 10)
>>> len(r)
    9
>>> r[0]
    1
>>> r[1]
    2
>>> r[8]
    9

We know that this range only contains nine values, so we would expect the valid indices to run from 0 through 8. Meanwhile, we'll see an error message if we try to ask for an index other than those.


>>> r[9]
    Traceback (most recent call last):
      File "<pyshell#5>", line 1, in <module>
        r[9]
    IndexError: range object index out of range

Given that we know that we could use indexing to obtain the integers in the range, we could use a while loop if we wanted to use all of them. For example, this is how we might calculate the sum of the integers in a range.


r = range(1, 10)
index = 0
total = 0

while index < len(r):
    total += r[index]
    index += 1

print('The sum is', total)

However, even this is somewhat unsatisfying. What's the point of having a range object — something that's intended to make it easier to work with a sequence of integers — if we have to manually manage indices like this? It turns out that there's a simpler way to approach this.


Iteration using the "for" loop

Given an object that contains a sequence of other objects, a for loop is used to iterate through that sequence, with the loop's body executing once per each of its objects. A sequence is actually a somewhat technical term in Python — and we'll explore that in some detail in ICS 33 — but you can think of it, for now, as being an object that contains a collection of other objects that are stored within it. Ranges certainly fit that description: They contain a collection of integers. So, we should be able to use a for loop to iterate through a range.

Syntactically, what we need to say are three things:

How we say those things is demonstrated below, a much simpler way to calculate the sum of the integers in a range than the version that was written above using a while loop.


total = 0

for num in range(1, 10):
    total += num

print('The sum is', total)

This gives us a handy way of writing a loop that runs a predetermined number of times — by looping over a range with the appropriate number of integers in it, even if we don't care so much what the integers are. For example, it we wanted to print Boo 15 times, we might write this:


for i in range(15):
    print('Boo')

As we'll see later, ranges aren't the only kind of object that can be treated as a sequence; a for loop will also allow us to iterate through the elements stored within a data structure, for example. Our general goal will be to use the for loop when we can, and a while loop when we must.

A few additional details

Like while loops, for loops permit the use of the break statement within their bodies, which would have a similar effect: stopping the loop immediately, with control flowing to the statement immediately after the loop.

Also like while loops, for loops can have an else clause, which is executed only if the loop ends normally. What it means to "end normally" is that the entire sequence is iterated, as opposed to reaching a break statement that ends the loop early.