4. Basic operations and types

4.1. Python as a calculator

At its most basic, Python can be used as a calculator to perform standard mathematical operations:

25 * 4.7

Most of these translate directly from pen+paper ("math expression") into a program ("code expression"), as in each of the following examples:

Math expression

$\rightarrow$

Code expression

4+8

4 + 8

217.81 \times 3

217.81 * 3

18 \times (29182 - 9382)

18 * (29182 - 9382)

-305.8 / 21.1

-305.8 / 21.1

The spacing between the numbers and the operators within a line does not matter; thus, 3+2 and 3+ 2 and 3 + 2 all evaluate equivalently. However, it is important to make the code readable to humans, so it is useful to be consistent in form: 3+2 or 3 + 2 might be preferable. (Later, we will see that spacing at the beginning of a line does have important meaning in Python.)

Since programming's roots are in performing mathematical expressions on a machine, Python contains many of the basic operators that we might expect: addition +, subtraction -, division / and multiplication *. These are in a family called binary arithmetic operators, because they perform basic arithmetic on two values (called operands): one to their left and one to their right, e.g., 10 + 31. Additional binary arithmetic operators include:

  • The integer division operator: //.
    Here, the value of the quotient is returned with the remainder discarded, so that 9 // 2 evaluates to 4, and 45 // 7 evaluates to 6.
  • The remainder operator (also called the modulo operator): %.
    Essentially, % is the complement of //, because it produces only the remainder from division. Thus, 9 % 2 evaluates to 1, and 45 % 7 evaluates to 3.
  • The power operator (or exponentiation): **.
    To square five, one would write 5**2. Fractional and negative powers may be used, so that the square root of seven could be written as 7**0.5, and the cube of one half could be written 2**-3.

The order in which Python performs operations in expressions is called operator precedence: operations with "higher precedence" are evaluated first. Much of this mirrors the standard mathematical hierarchy of the order of operations. For example, parenthetical expressions (...) have precedence over exponentiation, which is evaluated before multiplication and division, which in turn are evaluated before addition and subtraction. So, 3 + 4 * 5 evaluates to 23, not to 35 (and spacing within the expression would not affect this). Both % and // are grouped with division and multiplication. We will meet further operators below, such as logic operators and comparisons, and will discuss their relative precedence in evaluation, too.

An example of a Python operator that has only one operand is what we would consider "negation" in mathematics: -3. In this case, the - is a unary arithmetic operator, operating on one object to its right ("unary" means one, as opposed to "binary", which means two):

  • - as unary operator, when it only has a value (operand) to its right (and either no value or an operator to its left):

    - 20
    7.5 ** -2
    
  • - as binary operator, when it has an operand on both the left and the right (parentheses are not an operator; their interior resolves to a value, which forms the operand on the left):

    3-4
    (3 + 1) - 10
    

(And + can also be either a unary or binary operator in the exact same way as -. We just tend to use the former less, since +2 is the same as the simpler 2.)

Python internally distinguishes when - is a binary arithmetic operator (performing subtraction) and when it is a unary arithmetic operator (signing a value) by context; the spacing doesn't matter, but instead the Python interpreter checks what is to the left and right of the operator. The thing for us to know is that the binary and unary forms of - have different operational precedence, so we have to be careful to not mistake them.

Q: Think about what the results of each of the following expressions should be, and then check by evaluating each line in Python (which will have to be done individually---as written, Python won't output each line's evaluation, just the final one; we will see how to output multiple values simultaneously soon):

 13**2
 2-3**2
 33 ** -2
 43 ** 2 - 2
 53 * 2-2
 610+3 * 2
 7-+-++-5
 810 // 3 * 2
 911 // -4
109**0.5 + 1
114**(-0.5)
1223 % 5 % 2
1325 % 5
145 % 25

Note

From the above examples, note that the order of precedence with - and ** depends on whether the minus appears occurs in the base or in the exponent.

If we are ever uncertain about operator precedence in this or other cases, we can check a simple test expression or two. This quick testability is one great convenience of having interactive Python environments like IPython or Jupyter-Notebooks available for quickly running small pieces of code. Additionally, we can always use parentheses to simplify and clarify interpretation visually. We want the code to be both correct and readable.

4.2. Errors and debugging

Errors, mistakes, typos and bugs (the last, charmingly named when computational machines were much larger and actual bugs could cause mishaps) will occur. So what happens if we make a mistake with syntax when typing an expression---how will Python cope with this? The following line is missing a second operand for the binary power operator:

4 + 3**

This causes Python to give us an error message:

  File "<ipython-input-8-46aaa8163a8b>", line 1
    4 + 3**
           ^
SyntaxError: invalid syntax

Note how Python helpfully tries to tell us where the problem is: it specifies that the error occurred in "line 1" (later we will have multiline commands) and then uses the ^ symbol like an arrow to point to an even more specific part of that line where it got confused (recall, this error is missing an operand after **). Then it also describes the nature of its complaint: it classifies it as a SyntaxError (and that indeed was kind of error we intentionally made here). A syntax error might occur if an expected item is missing (like a value) or added (like extra symbol), and we will see examples of other kinds of errors as we proceed.

Here and in the future: Don't be afraid to see error messages. The Python interpreter is trying to help us to debug the code (that is, to solve the problem). An error message provides useful information (hence, the "message" part), and with more experience it becomes easier to understand it and to see what is happening in the code. There can be subtleties with debugging. Sometimes the actual error is something that occurred earlier in the code than where it points, causing Python to get confused when it reaches that spot. And sometimes there are multiple errors---when this happens, we should go to the first error message given, and start solving progressively from there. We can help ourselves greatly if we test pieces of our code, and run our code often, so that we are more likely to know immediately when new errors pop up and narrow the range of where they might be happening. This is a good habit to start developing early on.

Even experienced programmers get error messages when they write code. But being experienced means that they understand more of the terminology and have likely seen similar messages before, so that they can typically resolve their coding issues more quickly. Debugging will become much easier as we get more coding experience. For this reason we will frequently point out possible errors and common error messages in given situations, and it is worth making mental note of them, as well as other ones you happen to encounter. The sooner you become comfortable with reading them and debugging, the more efficient and happier you will be while programming.

We just have to make sure we fix our code in an appropriate way. Going back to the above case, we could do so perhaps as follows:

4 + 3**2

Note that there are more subtle kinds of errors, those that the Python interpreter won't flag. Consider if we had accidentally left our finger on the 2 key too long above, and typed:

4 + 3**222

In the context of the calculation, we entered the incorrect number---but this is not a mistake that the Python interpreter will detect. These are bugs that we, the humans, will have to squash on our own, with testing, testing and more testing of the code; even if it results in a failure later in the code, we will have to track down the original badness. We wish that Python could detect these kinds of mistakes, and we can even put "sanity checks" in our code to help catch things like this. But this is also part of the art of programming, and why it is important to have an expectation of outcomes of the code.

4.3. Exactness (or not) of calculations

What should we expect from our computed results? Are they always, 100% correct? Well, what do we even mean by "correct"?

Firstly, as with all machines, computers are not perfect. But the odds of some random, internal error producing a result that evaluates 3+5 as 7 is negligible. While it is possible that random errors can occur to affect calculations, the odds of this on a healthy computer are extremely small, and there are even underlying corrective checks in place. In practice we shouldn't worry about these kinds of "machine" errors occurring.

Secondly, programmers are certainly not perfect, and unfortunately the odds of coding mistakes (bugs, introduced above) are not negligible. Python can detect certain kinds of errors (such as syntax-based mistakes) and alert us to these with error messages, but there are still many user errors that will not be caught by the internal debugging tools (e.g., typing 4 instead of -4). We as programmers need to test and re-test our code to reduce the number and likelihood of these. This effort of validating and checking our code is one of the main arts of programming.

Thirdly, computers are finite machines. In mathematics, a number can be arbitrarily large, even to the point of using infinity. However, a computer cannot calculate with a number so big that it won't fit into the memory or disk space. There is always some finite maximum value above which we cannot calculate: infinity won't fit gracefully into computer memory. In Python version 3, this limit tends to be quite large, and it will often be above any number that would be realistically calculated, which is convenient. (But note that different programming languages might have very different maxima; and if you work with very large numbers, you want to check the maximum to make sure you don't reach it!)

Relatedly, the finitude of computers means that we also cannot represent a number with so many decimal places that it exceeds computer memory. In fact, we will have to account for this decimal finiteness quite often, and perhaps at a lower number of decimal places than one might expect. Consider this simple division: 2.0/3.0. What is the result when Python evaluates it? In mathematics the result would be 0.\bar{6}=0.66666666... with an infinite number of decimal places. However, the computer has finite disk space and memory, and here outputs only 0.6666666666666666, even though the computer memory is larger than 16 demical places. In general, we cannot expect the computer to have an exact representation of such numbers, nor of irrational (\sqrt{2}, 7^{1/3}) nor of transcendental numbers (\pi, Euler's e, etc.; though mathematicians also have difficulty exactly representing this last kind, at times...).

The finitude of computers will always be something to keep in mind when dealing with values that contain decimal points (called floating point numbers or just floats, discussed more below). There is a limit to the degree of precision (or just precision) with which a computer can evaluate expressions, which determines the maximal number of decimal places are used in the output. If this limit is reached, one of two outcomes can occur, depending on the evaluation: truncation, whereby the remaining digits get chopped off; or rounding, which can occur by different rules. Thus, decimal (or "floating point") expressions have a finite level of approximation, which can be loosely thought of as "being correct to a certain number of decimal places" (decimal precision).

A consequence of having finite precision is that we must view all float evaluations as approximations. The digit at that last place of precision might not be consistent over repeated calculations or on different machines (e.g., due to varied rounding rules). A degree of difference from analytic calculation will exist, called truncation error or round-off error, depending on the underlying mechanism. While we might be tempted to ignore such a tiny error that might be, say, of order 10^{-15}, it still means that we can have no expectation of exactness of computer-based calculations with floating point numbers. Also, if we have millions of calculations within a program, those random errors can accummulate into a meaningful difference. And while a particular floating point calculation might lead to equality on your computer, it might evaluate differently after a software update, or on another computer, or due to some other uncontrollable change. We will see later that some floats without many decimal places even have issues. Even within a single calculation, the round-off error and its instability might have meaningful consequences, so we must always treat floating point calculations as approximations.

Note

Python has a syntax for scientific notation (as do many programming languages). To express, say, 1.23\times10^{4} or 5.678\times10^{-9}, one would use 1.23e4 and 5.678e-9, respectively. The "e" is for "exponentiation." Equivalently, one could use a capital "E" and write 1.23E4, etc.

We will see this notation often when discussing precision and roundoff error, such as when an expression evaluates to a tiny number of order 10^{-16} (or 1e-16, in Python), instead of to 0.

In summary, both programmers and computers have some fundamental limitations that affect the way we will go about programming. There are many calculations that we can treat as exact, but we must be aware that a very large, important class of them cannot be. We can still use programming to usefully translate mathematics into operations the machine can do for us, but we will have to be careful in our own programming practice and in the kinds of calculations we ask a computer to perform. This section should convince you of the following:

  • Floating point results may have small precision errors, making exact results (from analytic calculations) difficult if not impossible to obtain with floats;

  • Differences involving the results of floating point arithmetic may lead to tiny rounding errors instead of to an exact zero, which may lead to problems in further calculations.

  • At all times when programming, we should have some knowledge of expected output, so that we can test and verify both pieces of code and the overall results.

  • Exactness is a tricky concept in many computer calculations. When working with floats (decimal points), don't rely on it.

Computers are useful, but they must be used with care!

4.4. Type, with some mathematical examples

Above, we have started noting some special considerations of numbers that have decimal points in them. One question that might be asked is: what is the difference (if any) in Python between the following calculations:

3 * 4
3.0 * 4.0

? The first evaluates to 12, and the second evaluates to 12.0. While these numbers have the same value, they do have an important difference: mathematically speaking, the first appears to be an integer, and the second appears to belong to the (rational) real numbers.

In many applications we take for granted the mathematical sets that numbers belong to, like integers (\mathbb{Z}) or reals (\mathbb{R}). But there is an analogous feature in programming which we will not be able to ignore: every quantity and object---even the non-mathematical ones---belongs to some well-defined category, and this fundamental property is called its type. The type property relates to how much space is allocated for an object on the computer, what operations can be performed with it, and how the utilized operators behave. There is a built-in function in Python called type() to tell us the type of any object, and applying it here we can see that: type(12) is int, and type(12.0) is float.

When performing calculations, we must consider both the value and type of the quantities involved. There are several types corresponding to familiar number sets from mathematics. In many cases the names are similar, but when programming we should use the computational name for clarity. We have noted some specific considerations that computational types have as opposed to their mathematical set analogues, mostly due to the finite nature of computers. Below are some examples of commonly used numerical types:

Comp type

Related math set

Examples

Comment

bool

Booleans \mathbb{B}

False (or 0), True (or 1)

There are only two values in this set. False is equivalent to 0, and True to 1.

int

Integers \mathbb{Z}

0, 21, -1000

The math set goes to \pm \infty, but the computational int has finite boundaries.

float

Real numbers \mathbb{R}, more specifically rationals \mathbb{Q}

0., 21.32138, -1000.312

The precision of floats is limited, and they have finite magnitude.

complex

Complex numbers \mathbb{C}

0+0j, -3+2.4j, 400.5-1j

The real and imaginary components are each floats, and therefore bound to finite precision and finite magnitude. In Python, j=\sqrt{-1} is the imaginary number.

It is worth stressing again that the mapping between most computational types and math sets is close but not perfect. This is particularly true for floats, because they have a finite precision, and therefore they can only represent a subset of the rational number set. The same is true of complex numbers, since each of their imaginary and real components are floats. The size and precision of all numerical values is eventually limited by computer size or other practical constraints.

When performing operations, what determines the type of the result? In Python, it is a combination of the input(s) and the operator(s) in the expression. Adding ints produces an int, as does multiplying ints. Adding or multiplying floats produces a float. However, some operators may lead to the result having a different type. For example, an int divided by an int yields a float, even if the result contains no remainder: 6/3 evaluates to 2.0 and 3/10 evaluates to 0.3; if the results stayed ints, then they would evaluate to 2 and 0. This is an example of Python performing implicit type conversion, whereby the interpreter is producing a type different than that of the elements of the expression, in order to reduce possible truncation.

Note

Not all programming languages behave the same way. For example, in C and even in the earlier Python 2.7, standard division of two integers produced another integer, so that 3/10 would evaluate to 0 (basically, performing integer division, which would require // in Python 3).

What happens when types are mixed? Evaluating 3.0 * 4 yields 12.0, a float. Here again Python is performing implicit type conversion: when mixing types in an expression, the result will be of the "most general" type in the expression. In this example, the more general type is float (in the sense that int values are a subset within floats).

The programmer can also have a final say on the type of a quantity, by performing explicit type conversion: using Python functions to specify the desired type. For example, float(3) yields 3.0, and int(3.4) yields 3. The output of bool() used on any numerical type that is nonzero will be True, and anyone that is exactly zero will be False: thus, bool(3), bool(-3) and bool(0.0) evaluate to True, True and False. Bool conversion is a useful "zero detector" because of this.

The complex type can be a bit tricky to deal with: complex(-1.0) yields (-1+0j), but trying to invert the process with float(-1+0j) yields an error. The latter is not a well-defined mathematical operation, namely because the complex type essentially has two components, while the float is scalar. However, bool(-1+0j) is still defined---it is True here because the complex number is nonzero. Complex numbers are discussed more in a later section.

Q: What type does the following output?

type(4.5 + 3.5)
+ show/hide response

Q: Think about what the type of each of the following is, and then put each expression within the type(..) function to check:

14
2-4
3float(4)
40.0
5int(float(True))

4.5. Practice

First come up with what you think the answer should be, and then check the result using an ipython terminal:

  1. What are the types of each of the following?

    1-1
    220.00001
    310**19
    410**19 - 10**19
    51 + 0j
    61
    7-2.
    8False
    
  2. What are the values (and types-- difficult properties to separate in some cases!) of the following?:

     12 + True
     24.5 - 3
     34.5 / 3
     45 + 3
     55 / 3
     65 / 3.
     75 // 3.
     84.7 % 3
     9float( 1 //4 )
    103. * 15 // 4
    11False * True
    12False and True
    
  3. Is 18 a factor of 12345678? Use two different operators to check.

  4. What are the category(ies) of each of the following operators:

    5 - -1
    
  5. In math, we could write 12(4+3) and evaluate it. Copy this exact this exact expression directly to Python (without changing/adding any symbols); what error do you get, and why? How can you write a correct expression for this in Python?

  6. Is this the correct way to calculate cube root of 27? If not, why not, and how could one adjust it to provide that information?:

    27**1/3
    
  7. Let's take the number 246897531. Write a single expression using only the modulo and integer division operators to output its hundreds digit (and which will keep working for any number we replace as a starter).

  8. Does Python produce the correct answer for the following: 0.1**2? Why or why not? (Hint: "correct" mathematically might differ from "correct" computationally.)