Famous martial artist and actor Bruce Lee once said

If you want to learn to swim, jump into the water. On dry land, no frame of mind is ever going to help you.

Programming is one of such a similar endeavor. As we move along, it is highly recommended that you execute the code and tinker with the program to understand it better. Simply, reading the code won't be much useful than writing code and executing it.

From this chapter onwards, we will be looking at a lot of Python code. I hope you have followed the guide and correctly set up the python development environment.

A Quick Tour of Python

Python has two modes: script and interactive. In normal mode or script mode, a file with .py extension is executed or run using the Python interpreter. These files are referred to as Python script or script, hence the name script mode.

Interactive mode is a command-line shell that gives immediate feedback for each statement. We will start learning Python using the Interactive mode. Depending on which operating system you use, start Python in the interactive mode accordingly.

Interactive Mode

The interactive mode contains a prompt where you provide the expression or statement you wish Python to evaluate.

>>> 		# Prompt

Prompt in Python is shown using three consecutive > symbols, as shown above.

When you write an expression or statement, the interpreter evaluates the expression or statement and outputs the result in the next line.

To understand this, let's start using Python as a calculator and see how you can work with numbers in Python.

Numbers

The first thing you should know about Python is: Almost everything is an object, and every object has a type. Let's first look into the type of objects which Python considers as numbers and use Python as a calculator using them.

Try executing the following commands and check if you get the same result.

>>> 4 + 5
9
>>> 4 + 74*5
374
>>> 74 - 5*5
49
>>> 50/8
6.25

The operators +, -, * and / can be used for addition, subtraction, multiplication, and division.

Python also contains the modulo operator (%), which returns the division's remainder.

What is the remainder when you divide 50 by 8?

  1. 0
  2. 2
  3. 4
  4. 6

The remainder of a division is often useful in programming. We can check the remainder of a division operation using the module operator %.

>>> 50 % 8	# Modulo Operator gets the remainder
2

If you use the division operator /, it returns the quotient. This form of division is called true division.

>>> 50 / 8	# True Division
6.25

Python also provides floor division operator (//), which discards the quotient's fractional part.

>>> 50 // 8	# Floor Division discards fractional part
6

In the above code samples, comments start with hash operator (#). Comments are discarded by Python while executing the source code.

If Python discards comments, why do you think programmers write them?

Programmers write comments to clarify the code and make it easier to understand what the code is doing for other programmers and even themselves.

There is a difference between the number 6.25 and 6 in the above codes sample. For Python, these two objects are of different types belonging to two different classes. However, both of them are considered numbers.

The numbers 50, 8, 6 are of type int or integers while numbers such 6.25 have type float or floating-point numbers. The main distinguishing characteristic between integer and floats is that floats have a decimal part along with a decimal marker (.).

You can check the type of an entity in Python by using the built-in function type().

>>> type(50)		# Integer
<class 'int'>
>>> type(50.0)		# Float
<class 'float'>

What do you think is the type of the following expression? Try it out in Python.

>>> type(25 + 25.1)
  1. <class 'int'>
  2. <class 'string'>
  3. <class 'float'>
  4. <class 'tuple'>

To evaluate the expression 25 + 25.1, Python converts the integer type object 25 to float type object 25.0.

Then it adds both the number to get the resulting float object 50.1, which is a float.

We can also get powers of a number by using the ** operator.

$$
3^6 = 729 \tag{1}
$$

Statement 1 states that the sixth power of 3 is 729, and we get the same result when we evaluate Python's expression.

>>> 3**6
729

What does the following expression evaluate to?

>>> 2**-1
  1. 0.2
  2. 0.4
  3. 0.5
  4. 4

You can also group expressions before applying operators to it using parenthesis () or parens for short.

Using parens, you can control the outcome of the expression.

There is a hierarchy of operators from which Python decides which expression to evaluate first. In the code below, the results differ depending on which operation Python performs earlier. Python performs the operations in the innermost parens earlier than the outer one.

>>> 2**(5//2+1)
8
>>> 2**5//2+1
17
>>> 2**(5//(2+1))
2
>>> 2**5//(2+1)
10

What is the result of the following operation?

>>> ((4**2)//5) - 1
  1. 0
  2. 1
  3. 2
  4. 3

You can assign names to an object or expression in Python by the assignment operator (=). The names are also be called identifiers or variables.

>>> guests = 340	# Assigning the name `guests` to the integer object 340
>>> tables = 2*3	# Expression is evaluated and resultant object is named

After you assign a name to an object, the interpreter displays no result and skips over to the next prompt. You can type the name and press the Enter key if you want to look up the object referenced by the name.

>>> guest_number
340
>>> tables
6

The type of names guest_number and tables corresponds to the type of the object it refers to; therefore, both are of type int or integers.

>>> type(guest_number)
<class 'int'>
>>> type(tables)
<class 'int'>

You can also use operators on the names as you would have done for numbers.

>>> guest_numbers + 50
390
>>> tables / 2
3
>>> guest_numbers // tables
56

In this case, Python figures out the object's value referenced by the name and then proceeds to operate.

What is the final value of name guests in the following code sample?

>>> guests = 300
>>> guests = guests / 5
  1. 30.0
  2. 40.0
  3. 50.0
  4. 60.0

In Python, we can also compare two objects.

We use the assignment operator (=) to assign names to objects while using the double equal sign ( == ) to check if two object's values are equal.

Let's check out some comparisons in Python.

>>> tables = 2*3
>>> tables == 6		# Is the name `tables` has value equal to 6?
True				# Yes

When you use the == operator, it is similar to asking Python if the two values are the same. Because this is a yes or no question, Python returns either True or False.

You can also check if a value is less than or greater than another value using < and > operators.

>>> 6 < 2**4		# Is 6 less than 2**4 ?
True				# Yes
>>> 7 < 5*1			# Is 7 less than 5*1?
False				# No

What is the type of True or False object?

  1. <class 'str'>
  2. <class 'bool'>
  3. <class 'complex'>
  4. <class 'list'>

The True or False values in Python have the type bool. The type bool is short for Boolean. We can check the type of the True or False object using the type() function.

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

The underscore symbol (_) acts as a built-in name available in the interpreter, which returns the last result, useful when doing subsequent calculations.

>>> 1 + 4
5
>>> _ 				# Refers to 5
5
>>> _ *5
25
>>> _  + 40			# Refers to 25
65
>>> _ / 13			# Refers to 65
5.0

If you assign the underscore (_) name another value, it overwrites or masks its built-in functionality.

What will Python evaluate in the following code sample?

>>> 1 + 4
5
>> _ = 56
>>> 24 * 5
120
>>> _
  1. 120
  2. 50
  3. 5
  4. 56

The Python interpreter throws an error if you try to refer to an undefined or unassigned name. We should define every name first before evaluating it in Python.

>>> lizard
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'lizard' is not defined
A NameError means that Python couldn't find the name you refer to.

In the previous code, the name lizard is not defined. Therefore, Python raises NameError. A mistyped name or undefined function name usually causes such an error.

What do you think is the result of the following object?

>>> animal = "Lion"
>>> anmal
  1. Lion
  2. Raises SyntaxError
  3. Raises NameError
  4. Raises DivisionByZeroError

Apart from the int and float, Python also supports other types of numbers, such as Fractions, Decimal, and Complex numbers. All of these are together are part of the numeric data-types of Python. We will cover them in detail later on.

Strings

Often, you require text characters to be displayed for users. Python can handle text characters in the form of strings. Python interprets any set of characters inside matching single quotes (') or double quotes (") as a string.

>>> "Hi, There !"	# String using double quotes
'Hi, There !'
>>> 'Helloooo' 		# String using single quotes
>>> type('Helloooo')
<class 'str'>		# Type

A string object has type str. If you don't provide matching quotes, Python will raise an error.

>>> 'A cat's life is the best'
File "<stdin>", line 1
    'A cat's life is the best'
           ^
SyntaxError: invalid syntax

If you wish to use a single quote in your string, use double quotes to create the string. Similarly, if you wish to use double quotes in the string, then use single quotes.

>>> "A cat's life is the best"
"A cat's life is the best"
>>> 'The cat said, "Worship me, hooomans !"'
'The cat said, "Worship me, hooomans !"'

The single and double quotes are special characters in Python.

Why do you think the quotes are considered special characters in Python strings?

As single and double quotes are used to create strings, they are considered special characters. Due to their special functionality, the usage of single and double quotes is illegal inside single and double-quoted strings, respectively.

Now, let's look at how we can display the characters on the screen.

Printing Strings

The built-in function print() produces a more readable string format and is mostly used to display characters on the screen.

>>> "Hello World."
'Hello World'
>>> print("Hello World")
Hello World

We can assign names to strings to refer to them later on. To print multiple strings, we can provide multiple strings to the print() function separated by a comma.

>>> first_name = "Ted"
>>> last_name = "Chiang"
>>> print(first_name, last_name)
Ted Chiang

When you provide comma-separated arguments to the print() function, they are displayed with a space between them, by default.

What is the output of the following Python code?

>>> greet = "Hey,"
>>> name = "Luffy"
>>> print(greet, name, "!")
  1. Hey, Luffy!
  2. greet Luffy!
  3. Hey, Luffy !
  4. HeyLuffy!

Escape Sequences

To insert characters that are illegal in a string, we can use an escape character or sequence. Escape sequences are used to insert a set of characters with some special meaning for a Python string in the print() function. Escape sequences usually start with a backslash \ followed by a character. We can insert the double quotes (" ) using its corresponding escape character \".

>>> "This is called \"escaping\""
'This is called "escaping"'

By escaping the special characteristics of double quotes (" ), we can use the quotes inside a double-quoted string.

Other escape characters that are often used inside the string are shown in table 1.

Table 1: Escape Sequences
Code Result
\\ Backslash(\)
\' Single Quote (')
\" Double Quote (\")
\n ASCII Linefeed (LF)
\r ASCII Carriage Return (CR)
\v ASCII Vertical Tab
\t ASCII Horizontal Tab

Similar to the quotes, the backslash (\) has a special characteristic inside strings. It lets you write a string spanning multiple lines by adding the backslash at each line's end. The resulting string doesn't contain any new lines.

>>> line_1 = "This is \
... going to be a \
... quite a long line, \
... isn't it?"
>>> line_1
"This is going to be quite a long line, isn't it?"

Whenever you insert the backslash character inside a string, Python interpreters it as the line-continuation character and skips over it. To insert the backslash character, we need to instruct Python to ignore its special characteristic and use it as it is.

To escape the special characteristic of the backslash, we can use the escape sequence \\.

>>> line_2 = "This is also \
... a long line \
... with a backslash (\\)"		# Escape Sequence
>>> print(line_2)
This is also a long line with a backslash (\)

Carriage Return & Line Feed

ASCII defines two special characters for newlines Line Feed (LF) and Carriage Return (CR).

A line feed means moving one line forward. The escape code is \n. The line feed moves the cursor down to the next line without returning to the line's beginning.

>>> print("Hello\nWorld")
Hello
World

A carriage return moves the cursor to the beginning of the line without advancing to the next line. The escape code is \r. If there is some text written in the line, this would mean overwriting the text.

>>> print("Cool I am \rFool")
Fool I am

Text editors in Windows still use both as \r\n in text files. Unix-based OSs use mostly only the \n.

The separation comes from the times of typewriter when we used to turn the wheel, displacing the paper to change the line, and move the carriage to restart typing from the beginning of a line. This was two steps.

What is the output of the following code?

>>> print("Deus vult\rSo What !")
  1. Deus Vult.
  2. So What !
  3. None of the above
  4. Deus vult So What!

Horizontal & Vertical Tabs

ASCII defines two types of tabs: horizontal(\t) and vertical (\v). Their usage in strings is shown below.

>>> print("I need\t some\t horizontal\t space")
I need	 some	 horizontal	 space
>>> print("I\v need\v some\v vertical\v space")
I
  need
       some
            vertical
                     space

Multiline Strings

When we want our strings to span multiple lines, we can do so using the escape sequence for the newline \n.

>>> print("Hi there. \nGood day to you.")
Hi there.
Good day to you.

Another way to create multi-line strings is by using using triple quotes: """...""" or '''...'''.

>>> poem = """
Beyond this place of wrath and tears
      Looms but the Horror of the shade,
And yet the menace of the years
      Finds and shall find me unafraid.

It matters not how strait the gate,
      How charged with punishments the scroll,
I am the master of my fate,
      I am the captain of my soul.

-- WILLIAM ERNEST HENLEY, Invictus
"""
>>> print(poem)
Beyond this place of wrath and tears
      Looms but the Horror of the shade,
And yet the menace of the years
      Finds and shall find me unafraid.

It matters not how strait the gate,
      How charged with punishments the scroll,
I am the master of my fate,
      I am the captain of my soul.

-- WILLIAM ERNEST HENLEY, Invictus

Concatenating Strings

Earlier, we used the + operator to add two numbers in Python. We can also use the + operator on strings. Let's try to figure out the possible usage of the + operator regarding strings using an exercise.

Which of the following is the output of the following code?

>>> quantity = "5"
>>> item = "apples"
>>> quantity + item
  1. 5apples
  2. 5 apples
  3. applesapplesapplesapplesapples
  4. None of the above

We can concatenate or add together two strings using the + operator.

>>> "16" + "$"
'16$'
>>> quantity = '5 '
>>> item = 'apples'
>>> quantity + item
'5 apples'

However, do not try to concatenate a numeric type with a string. If you try to add a number with a string, Python will raise TypeError, informing you that it is unsupported.

>>> 5 + "apples"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

As you can notice from the above code, adding an integer type to a string is not supported in Python.

Two or more strings next to each other are automatically concatenated.

>>> "Hello" "World" "!"
'HelloWorld!'
>>> "I" 'ron' 'Man'
'IronMan'

Repeating Strings

We can repeat strings n number of times by using the * operator.

>>> "Me"*10
'MeMeMeMeMeMeMeMeMeMe'
>>> ("You & " + "I, ")*5 + " in this beautiful world !"
'You & I, You & I, You & I, You & I, You & I,  in this beautiful world !'

String Length

Strings are a sequence or collection of characters. If you wish to know the number of characters in a given string, you can use the built-in len() function.

>>> greet = "Hello There. How are you doing?"
>>> len(greet)
31

What is the result of the following code sample?

>>> len("Hi"*10)
  1. 21
  2. 22
  3. 19
  4. 20

The len() function gives the total number of elements in a collection. The len() is a common operation across all collections in Python. Another common operation across many collections uses the position index of the element inside the collection to access the element.

Indexing in Strings

Accessing an element in a collection by using its position index is called indexing. Python also provides a way to access each character in the string by indexing.

The first character of a string has index 0 and can be accessed using a square bracket ([]).

>>> course = "Python - I"
>>> course[0]			# Return the character at 0 index
'P'
>>> course[7]			# Return the character at 7th index
'-'

If you use negative index, Python returns an element starting from the string's end.

>>> course = "Python - I"
>>> course[-1]			# Return the last character
'I'
>>> course[-3]			# Return the third last character
'-'

As the index starts from 0, we can get the last element by subtracting one from the string's lengths.

>>> course = "Python - I"
>>> course[len(course) - 1] # Returns the last character
'I'

The negative index can be thought of as subtracting the index from the string's length.

>>> course[-1] == course[len(course) -1]
True
>>> course[- 5] == course[len(course) -5]
True

What do you think happens with the following code?

>>> course = "Python - I"
>>> course[100]
  1. Raises IndexError Correct
  2. Raises SyntaxError
  3. ""
  4. Raises KeyError

If you use an index greater than the length of the range of valid index range, Python raises an IndexError.

What do you think is the valid index range for a string of length 6?

The valid index range of a string of length six is the integers from -6 to 5. For a general string str, we can say that the valid index range includes integers from -len(str) to len(str)-1.

If you use an index apart from those integers, Python will raise an IndexError.

>>> greet = "Hello"
>>> greet[100]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

Slicing a String

We can get a substring of a given string by slicing the string. Getting a subset of a collection's elements by specifying the starting and ending index is called slicing.

$$
\text{Slicing = }  (\text{start-index, end-index, step-size)}
$$

If you don't provide the start index, Python assumes it to be 0, while it assumes the end index as the last element.

>>> course = "Python - I"
>>> course[1:]
'ython - I'
>>> course[:4]
'Pyth'

The default step-size is 1. If you provide a step size, say $n$, and start index $a$, Python returns the elements at the following indices.

$$
a, a + n, a+2n, a+3n, ....a,a+n,a+2n,a+3n,....
$$

For instance,

>>> course[1::2]
'yhn-I'

Let's do a quick exercise.

Which of the following is the following slicing operation's code output?

>>> title = "P00RUUIQQMVVETTR"
>>> title[::3]
  1. 'PRIMER'
  2. 'PRIQRT'
  3. 'UUIQTR'
  4. 'PUQVTR'

Objects which contain references to other objects are called container or collection objects. Python string is a collection of characters. Other collections include such as Lists, Dictionaries, Tuples etc.

List

A collection of comma-separated elements between square brackets is called List. There are no constraints on what type of items you can add to a list.

>>> names = ["Julius", "Octavious", "Brutus", "Cassius"]
>>> names
['Julius', 'Octavious', 'Brutus', 'Cassius']

You can add even different types of objects inside lists too.

>>> a_list = [5, 0.4, "0.5"]

We can also concatenate lists with other lists.

>>> names + ["Casca", "Cato", "Quintus"]
['Julius', 'Octavious', 'Brutus', 'Cassius', 'Casca', 'Cato', 'Quintus']

What does the following code returns?

>>> names = ["Julius", "Octavious", "Brutus", "Cassius"]
>>> names[3]
  1. Julius
  2. Octavious
  3. Brutus
  4. Cassius

Similar to strings, lists support indexing and slicing. The number of items in a list can be obtained by the built-in len() function. Let's check out some examples of Indexing below.

>>> names[0]
'Julius'
>>> names[-2]
'Brutus'
>>> names[:3]
['Julius', 'Octavious', 'Brutus']
>>> len(names)
4

The code listing below shows slicing operation in lists.

>>> names[0:4]
['Julius', 'Octavious', 'Brutus', 'Cassius']
>>> names[1:4]
['Octavious', 'Brutus', 'Cassius']

If you wish to change the list item at a specific index, you can assign it to a different item.

>>> names
['Julius', 'Octavious', 'Brutus', 'Cassius']
>>> names[-1] = "Calpurnia"
['Julius', 'Octavious', 'Brutus', 'Calpurnia']

Python reassigns the specific index to the new item you provided.

What do you think is the result of the following code?

>>> full_name = "Julios Caesar"
>>> full_name[4] = 'u'
  1. Changes the full_name to Julius Caesar
  2. Raises TypeError

The operation of modifying elements is not supported in the strings. If you try to modify a string, Python raises TypeError.

>>> name = "Julios Caesar"		# Oops got the spelling wrong.
>>> name[4]
'o'
>>> name[4] = 'u'				# Let's try to change the spelling
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

In Python, there are types of objects whose value can be changed after it's the creation and some whose value cannot be changed.

The type of objects that can be modified is called mutable type. The type of objects that cannot be modified is called immutable type. In Python, strings, numbers are examples of immutable type while lists are an example of mutable type.

Slice Assignment

In Python, you can assign new lists to slices of a list. This is called slice assignment.

You can use slice assignment to modify multiple list items or remove multiple items entirely. Slice notation works similarly to those of the strings that we covered earlier.

>>> two_powers = [1, 2, 4, 8, 10, 12, 128]
>>> two_powers[3]
8
>>> two_powers[4:-1] = [16, 32, 64]	# Assigning slices to list
>>> two_powers
[1, 2, 4, 8, 16, 32, 64, 128]
>>> two_powers[3:5] = []		# Assigning slices to blank list
>>> two_powers
[1, 2, 4, 128]
>>> two_powers[:] = []			# Assigning entire list to blank list
>>> two_powers
[]

What do you think will be the result of the following code?

>>> characters = ['Luffy', 'Sanji', 'Zorro']
>>> characters[::2] = ['Nami', 'Robin']
  1. ['Nami', 'Sanji', 'Robin']
  2. ['Luffy', 'Nami', 'Sanji', 'Robin', 'Zorro']
  3. ['Nami', 'Luffy', 'Sanji', 'Zorro', 'Robin']
  4. Raises ValueError

Slice assignment works a little bit differently than slice indexing. Let's try to understand the code in the previous exercise.

>>> characters = ['Luffy', 'Sanji', 'Zorro']
>>> characters[::2]
['Luffy', 'Zorro']							# Slice
>>> characters[::2] = ['Nami', 'Robin']	# Replace the above slice
>>> characters
['Nami', 'Sanji', 'Robin']

As you can see, Python replaces the slices with the new list object. It leaves the objects, not in the slice, untouched.

Describe the slice assignment operation in your own words.

Nested List

Apart from that, you can also put lists inside another list. This is called a nested list. We can access each item in the nested list using the nested list index, followed by the item's index in the nested list. Let's take an example to understand more.

Suppose we have a list b, which has two lists inside it.

>>> b = ['a', 'b', [1,2,3], [0.1, 0.2, 0.3] ]

To access the first list [1,2,3], we can use its position index 2.

>>> b[2]
[1, 2, 3]			# Nested List

To access an element inside the nested, we can again use the bracket notation with the index.

>>> b[2][0]
1					# Nested List item 1

Similarly, we can access the second nested list.

>>> b[3]
[0.1, 0.2, 0.3]
>>> b[3][-1]
0.3

What is the value of the below code?

>>> b = ['a', 'b', [1,2,3], [0.1, 0.2, 0.3] ]
>>> b[2][2]
  1. 2
  2. 0.3
  3. 3
  4. 0.2

Successive square bracket notations can access nested lists and their elements. You can think of the referencing via index as a name in itself.

To understand the idea better, let's look at the following code sample.

>>> pow_of_2 = [2, 4, 8, 16]
>>> pow_of_3 = [3, 9, 81, 243]
>>> combined = [0, 1, pow_of_2, pow_of_3]

We can access any element of the nested lists pow_of_2 and pow_of_3 from the combined list itself.

>>> combined[2][2]		# Accessing pow_of_2
4
>>> combined[2][3]		# Accessing pow_of_2
8
>>> combined[3][0]		# Accessing pow_of_3
3
>>> combined[3][3]		# Accessing pow_of_3
9

In mathematics, a matrix is rectangular array of numbers. We can represent a matrix of numbers such as the following:

$$
A =\begin{bmatrix}1 & 2 & 3\\ 4 & 5 & 6 \\ 7 & 8 & 9\end{bmatrix}
$$

We can represent the above matrix in Python as follows.

>>> matrix_A = [[1,2,3],
               	[4,5,6],
                [7,8,9]]
>>> matrix_A
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Now, we can access items inside the matrix using Indexing.

>>> matrix_A[1][1]
5
>>> matrix_A[0][0]
1
>>> matrix_A[2][2]
9

How do you think the matrix_A can be updated so that the second element of the second row becomes 0?

  1. matrix_A[2] = [4, 0, 6]
  2. matrix_A[2][2] = 0
  3. matrix[3][3] = 0
  4. matrix[1][1] = 0

Tuples

Tuples are container objects that cannot be modified after being created, unlike lists.

Tuples are created by enclosing comma-separated items enclosed in a pair of parenthesis () instead of square brackets [] or simply comma-separated items.

The code listing below shows some ways to create a tuple object.

>>> a = (1,2,3)			# Tuple within a pair of parenthesis
>>> a
(1,2,3)
>>> b = 1,2,3			# Tuple using comma-separated items
>>> b
(1,2,3)

To access elements inside tuples, we can use Indexing similar to lists.

>>> b[2]				# Indexing
3
>>> b[-1]
3

Tuples also support slicing operations.

>>> b[1:]				# Slicing
(2, 3)

As we cannot modify tuples after creation, they are immutable objects similar to strings. When you try to do an index-based assignment in tuples, Python raises an error.

What is the output of the following code?

>>> a = 1, 2, 3
>>> a[2] = 5
  1. Tuple a becomes 1, 2, 5
  2. Raises TypeError
  3. Raises ValueError
  4. Raises SyntaxError

When you try to modify a tuple, Python raises TypeError, informing you that TypeError: 'tuple' object does not support item assignment. You can try it out in your Python console to check.

Let's now look at Python dictionaries.

Dictionary

In Python, dictionary is a container object, which is used to store items corresponding to a key. The keys are used to access the items. For example,

>>> a = { 'name': "Primer", "value": 100}	# Dictionary Object
>>> a["name"]
'Primer'
>>> a["value"]
100

You can add or modify the dictionary directly by using the corresponding key of the object.

>>> a["name"] = "Primerlabs"		# Modify
>>> a
{'name': 'Primerlabs', 'value': 100}
>>> a["utility"] = "Learning"		# Add new key-value pair
>>> a
{'name': 'Primerlabs', 'age': 100, 'utility': 'Learning'}

There are no restrictions on what values you can add to a dictionary.

We have covered a quick tour of Python using an interactive shell. Now, let's look into a sample Python script in the script Mode.

Script Mode

In this section, we will again take a quick tour in Python, though, in the script mode. Before that, can you recall what's the difference between interactive mode and script mode in Python?

In interactive mode, Python gives feedback immediately for each statement. In script mode, we will have to write a script, usually in a .py extension. Then execute the file using the Python interpreter.

Let's take the following sample script.

# store the given list of number
number_list = [8, 31, 27, 29, 48, 56]

# keep count of odd and even numbers
odd_numbers, even_numbers = 0, 0

for num in number_list:

    # checking condition
    if num % 2 == 0:
        even_numbers += 1 # x += 1 is equivalent to x = x + 1

    else:
        odd_numbers += 1



print("Even numbers : ", even_numbers)
print("Odd numbers : ", odd_numbers)
Listing 3: A Python script to count the numbers of odd and even in a given list, sample_script.py

You can open any text editor and create the above file. Save it with the name sample_script.py. To run the Python script, we will need to use the Python interpreter and ensure that you can access a working Python environment as described in the guide.

You should see the following on your screen when you save the program in a file and run this script using the Python interpreter.

>>> python sample_script.py
Even numbers :  3
Odd numbers :  3

The above program is a small script written in Python. Even if you are not aware of the Python programming language's complete syntax, you can still somewhat guess what the program is doing.

Readability is one of Python's biggest strengths, making it easier for beginner programmers to start with Python.

Guess what the above program does.

The above Python script counts the numbers of odd and even in a given list.

Let's walk through the code and discuss the syntax of Python. As you might recall, a programming language is guided by two essential rules: syntax and semantics. Let's look at syntax, which is the structure of the code.

Comments

The script starts with a comment.

# store the given list of number

We can write comments in Python using the # ( hash ) symbol. Python discards anything written after the # symbol. Comments serve as hints or clues to understanding the code written by a programmer.

Sometimes, what a piece of code does is not clear from simply reading. In such cases, programmers should write comments explaining why that piece of code is required.

An excellent way to write comments is to focus on why the code block is needed rather than what it does.

Multi-line Comment

Many programming languages support multi-line comments. The code listing below shows multi-line comments in javascript programming language.

/*
In javascript, 
this is a block comment.
It spans multiple lines. 
*/
How do you think we can add a multi-line comment in Python?

Python doesn't support multi-line block comment out of the box. We can add Multi-line comments by writing successive # symbols spanning several rows. The code listing below shows how to write a multi-line comment in Python.

# This is a multi-line comment
# that extends to three lines of
# code.

Comments following a Python statement in the same line is called an inline comment.

even_numbers += 1 # shorthand for x = x + 1 where x = even_numbers

Assignment Operation

The next line in the script is an assignment operation. We are assigning the name number_list to a list of numbers on the assignment operator's (=) right-hand side.

In Python, the end of the line signifies the termination of a Python statement.

number_list = [8, 31, 27, 29, 48, 56]

In case you want to continue writing a statement to the next line, you can use the backslash \ marker to do this.

a = 79*9 - 2*26 + 1 \
 + 6

print(a)
666.0

We can also extend a statement to the next line by wrapping the whole expression with parenthesis ().

a = ( 79*9 - 2*26 + 1
 + 6 )

print(a)
666.0

Assignment Styles

The next line is also an assignment statement in which we are assigning two different variables, the same value 0. There are different assignment styles which we can look into.

In the first assignment style, as shown in the script, we can assign two names by assigning them to a tuple of length two.

odd_numbers, even_numbers = 0, 0	# Assignment Style 1

We can also terminate a Python statement by using semi-colon ; and rewrite the above statement as

odd_numbers = 0 ; even_numbers = 0 	# Assignment Style 2

The above code is equivalent to assigning in two different lines.

odd_numbers = 0
even_numbers = 0

As we are assigning the same value to multiple variables, we can also use the following expression.

odd_numbers = even_numbers = 0		# Assignment Style 3

Depending on the context, we should use one of the three assignment operations shown above.

What is the result of the following code?

>>> a, b, c = (1, 2, 3)
>>> a ** (b*c)
  1. 7
  2. 14
  3. 1
  4. 0

Code Blocks

The next lines of codes relate to looping over the elements in the number list. The one thing that stands out in this text unit is that all lines are indented apart from the first line. Let's take a look.

for num in number_list:

    # checking condition
    if num % 2 == 0:
        even_numbers += 1 # x += 1 is equivalent to x = x + 1

    else:
        odd_numbers += 1

Indentation signifies an essential feature of Python - whitespace is meaningful in Python. Python uses leading whitespace (spaces and tabs) at the beginning of a line to compute the line's indentation level, which is used to determine the grouping of statements or code block.

Appropriate indentation and whitespace signify if a piece of code is inside a code block.

In other languages, code blocks are often demarcated by curly braces {} instead. Take the case of JavaScript.

if (hour < 1800) {
  greeting = "Good day"; // Code Block
} else {
  greeting = "Good evening"; // Code Block
}

The same is shown in Python using indentation.

if hour < 1800 :
  greeting = "Good day"  # Code Block
else:
  greeting = "Good evening" # Code Block

Using indentation for grouping is extremely elegant and contributes a lot to the average Python program's clarity. Using whitespace to demarcate code blocks enhances readability. At the same time, indentations are prone to syntax errors.

Using indentation has some rules that need to be kept in mind.

Every Python code block is preceded by a colon : on the previous line.

if True:
    # do something

In code, all sibling Python statements need to have the same indentation level; otherwise, Python complains about inconsistent indentation.

print("Hi there")
	print("How are you ?") # Wrong Indentation respect to sibling statement

Given that whitespace is important in Python, what do you think about the following code?

a=41+1   #1
a	=	41	+	1  #2
a		=		41		+		1 #3
  1. All valid
  2. Some valid, some invalid
  3. Statement #1 is valid, and the other two are invalid
  4. Statement #2 is valid

While Python is strict about whitespace usage in its code blocks, Python disregards the whitespace inside the line itself.

All of the three variations with different whitespace in the previous exercise are still valid expressions in Python.

The primary reason is to increase the readability of the Python statement.

Loops

Let's continue our discussion on the above sample script.

number_list = [8, 31, 27, 29, 48, 56]
...

for num in number_list:

...

The objects which Python can iterate over are called an iterable. In this case, the iterable happens to be a list. Python uses the keyword for to iterate through an iterable (iter-able).

The for keyword loops over the list in the above code, assigning the element as num while executing the code block inside the for a loop.

Control Flow Statements

 if num % 2 == 0:
        even_numbers += 1 # x += 1 is equivalent to x = x + 1
 else:
      	odd_numbers += 1

Inside the for loop, we check each number in the list to determine if they are odd or even by using the test of divisibility[1] by 2.

How can we know if a number is odd or even?

A number is said to be even if it is divisible by 2. This means the remainder upon division by two is 0. To test for divisibility by 2, we use the modulo operator (%), which we introduced earlier.

The modulo operator checks if the current number in the loop, referenced by the name num, checks if the remainder is 0. If the number is divisible by 2, then the object's value with the name even_numbers increases by 1. Otherwise, the value of the object with the name odd_numbers is increased by 1.

In programming, objects which are used to keep count are often called **counter variable ** or counters.

The if and else statements are called conditionals and are part of control flow statements in Python. The code block inside if is executed only if the condition supplied is True.

if num % 2 == 0:
    ...

If the condition is False, then the code block inside the else block is executed. Let's do a short exercise.

What is the result of the following code sample?

>>> if 15 % 5 == 0:
...     print("15 is divisible by 5")
... else:
...     print("15 is not divisble by 5")
  1. 15 is divisible by 5
  2. Raises ValueError
  3. 15 is not divisible by 5
  4. Raises TypeError

Parentheses

The last lines of codes remaining relate to the print function. To invoke the print() function, we can use the parentheses ().

print("Even numbers : ", even_numbers)
print("Odd numbers : ", odd_numbers)

A function accepts a tuple of arguments inside the parentheses. The print() function takes a string as the first argument to print on the screen , i.e., "Even numbers" followed by the value of the object referenced by the name even_numbers. The next print() function calls works similarly.

Earlier, we extended statements to multi-line by using parentheses.

a = ( 79*9 - 2*26 + 1
 + 6 )

In Python, parentheses are used for grouping and calling or invoking a function.

>>> x = (45*56)/(45)
>>> 56.0

We will learn more about functions in the next chapter.

What is the result of the following code?

>>> greet1 = ("Hi There !")
>>> greet2 = ("\rHow are you?")
>>> print(greet1, greet2)
  1. Hi There! How are you?
  2. How are you?
  3. Hi There!
  4. Hi There!
    How are you?

Built-in functions print() and type() are always available to the programmer in Python. However, Python has many standard functions that we need to import first to use them.

These are defined in standard modules of Python.

Module

Modules refers to a file containing Python statements and definitions.

A file containing Python code, for example, secrets.py, is a module, and its module name would be secrets.

secret = 42
secrets.py

A program in the same directory as that of secrets.py can access the definitions of secrets.py using the import keyword.

To test this out, create a file name secrets.py with the code shown above, and in the same directory, create example.py. Your directory/folder structure will be looking at something like this.

├── secrets.py
└── example.py
Directory Structure

Now, in the example.py, write the following.

import secret from secrets

print("The secret was", secret)
example.py

Executing the example.py should show you the following.

The secret was 42
Output of example.py

You can break larger files into smaller manageable and organized files using modules to provide reusability of code in programming. The import keyword is used to access definitions stored in modules.

Standard Modules

Python has tons of standard modules which comes along with the Python interpreter. One of the standard modules is the random module, which you can use to generate random numbers or distributions.

>>> import random 			# Standard Module

After importing a module, you can access the definitions stored inside it using the dot notation.

One useful function inside the random module is the randint(a, b) function, which generates a random integer between two integers $a$ and $b$, every time you invoke it.

>>> import random
>>> random.randint(1, 5)
4
>>> random.randint(1, 5)
3

Another way to use the definition inside a module is to import the definition explicitly using the from keyword. Doing so, let's us use the function without the module name.

>>> from random import randint
>>> randint(1, 5)
4

Let's do a short exercise.

What the result of the following code?

>>> import random
>>> random.randint(1, 10) % 2 == 0
  1. True
  2. False
  3. Will vary each time you execute

This brings us to the end of the quick tour of Python. This was a very small overview of Python to help you get a feel of Python as a programming language.

In the next section, we will look into the fundamentals of Python Programming Languages.

Objects & Literals

In a programming language, there are specific constant values that form the building blocks of that language. In the example in the previous section, we stored a bunch of numbers in a list.

number_list = [8, 31, 27, 29, 48, 56]
...

We also divided each number by 2 to check if the number is even or odd.

...
	if num % 2 == 0:
...

We did this with the assumption that the symbols such as 8 represents the number 8, which we can divide by other numbers. For this program to run correctly, the Python interpreter must not change the value of the symbol 8 to say, 5. Doing so would be a mess.

Therefore, Python defines certain items that describe a constant value and are interpreted as they appear. These are called literal constant or literals.

Assignment to a literal is prohibited in Python.

>>> 8 = 5
  File "<stdin>", line 1
SyntaxError: can't assign to literal

Earlier, we saw that the Python interactive interpreter evaluates the expression or the name and returns its result or value.

>>> 3 + 5				# Expression
8
>>> prime_numbers = [2, 3, 5, 7, 11]
>>> prime_numbers		# Name
[2, 3, 5, 7, 11]

For literals, the Python interpreter returns the same object.

>>> 1
1
>>> 0.1
0.1
>>> "Hello"
'Hello'

Literals are a sequence of one or more characters written exactly as you want Python to interpret.

There are two types of literals that we are interested in - string and numeric literals. Let's take a look at String Literals.

String Literals

String literals or strings represent a sequence of characters surrounded by a matching pair of either single quotes (') or double quotes (" ).

Let's do a short exercise.

What is the result of the following code?

>> "Hello" = "World"
  1. Raises SyntaxError
  2. Raises TypeError
  3. Raises ValueError
  4. Raises KeyError

In the previous exercise, "Hello" is a string literal. When you try to assign "Hello" = "World", Python informs you that you cannot assign it to a literal.

Mostly strings are used for writing text. The following code listing shows some of the string literals that you can create.

"A"			  # String with a single character
'Hello World' # String with multiple characters using single quotes
"100 $" 	  # Strings with non-letter characters
"     "		  # Strings with blank characters
""			  # Empty String

As we covered before, Python interprets any sets of characters wrapped in matching-quotes as a string, and Python interprets it as it is.

>>> "Hello"
'Hello'
>>> '1000'
'1000'
>>> """This is
... a mutli-line
... string literal"""
'This is \na multi-line\nstring literal.'

When we wrap a number with quotes, Python also considers it a string. Python cannot divide this string by another number as the string's division by a number is unsupported.

>>> "100"		# String
'100'
>>> 100/2		# Normal Division
50.0
>>> "100"/2		# Will raise error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'str' and 'int'

What do you think is the result of the following code?

>>> "Hello" - "ello"
  1. 'H'
  2. Raises TypeError
  3. Raises SyntaxError
  4. Raises ValueError

Although, Python does allow the use of multiplication operator (*) to repeat the string literal.

>>> " +-+ " * 3
' +-+  +-+  +-+ '

And you can also add together string literals.

>>> "Hello " + "World"
'Hello World'

As we covered before, Python interprets any sets of characters wrapped in matching-quotes as a string, and Python interprets it as it is.

>>> "Hello"
'Hello'
>>> '1000'
'1000'

Next, let's look at numeric literals.

Numeric Literals

A literal containing only digits (0-9), an optional sign character (- or +), and a possible decimal point is called a numerical literal.

There are three types of numeric literals: integers, floating-point numbers, and imaginary numbers.

Integer Literals

Integers without quotes are interpreted by Python to be integer literals.

Note that leading zeros in a non-zero integer number is not allowed. Also, when there is no sign, it is assumed that the number is positive.

>>> 12
12
>>> -45
-45
>>> 123_45	# underscore is valid
123456
>>> 000123 	# Throws Syntax Error

For readability, sometimes large numbers are grouped by separating with commas. For example, 1,000,000 is easier to read as a million instead of just 1000000. In Python, you cannot use a comma inside an integer literal as Python interprets it as a tuple.

>>> 1,203
(1,203)

However, you can use an underscore for the same purpose.

>>> 1_000_000
1000000

What is the value of the following expression?

>>> 1,245 + 45
  1. 1,250
  2. 1250.0
  3. (1, 290)
  4. Raises TypeError

In the previous exercise, the expression 1, 245 is a tuple. Therefore the addition of 45 works with regards to tuple, which we will cover in the later sections. Let's look at the next type of literal: floating points.

Floating-Point Literals

A floating-point value or float is a numerical literal that contains a decimal point. In a floating-point, there is no fixed number of digits before and after the decimal point. That is, the decimal point can float. Unlike floating-points, fixed-point representations have the number of digits before and after the decimal point fixed.

>>> 34.0
34.0
>>> 0.45_59
0.4559

You can add multiple $0$s in the floating-point literals' front or end, which is still valid.

>>> 000000003.5400000000
3.54

You can also write floating-points in exponential notation. When you write integers in exponential notation, they become a floating-point. In Python, $e$ or $E$ is used to write in scientific notation in the powers of $10$. Therefore, we can write,

$$
12e2 = 12 \times 10^{2} = 1200\\ 1e\text{-2} = 1 \times 10^{-2} = 0.01 \\ 4E\text{-2} = 4 \times 10^{-2} = 0.04 \\ 1e4 = 1 \times 10^4 = 10000.0
$$

To write in exponential notation, you can write e or E followed by the specified power of 10 at the end of a number. We can show the exponential or scientific notation in the following way:

>>> 12e2 	# 1200 written in exponential notation
1200.0
>>> 1e-2	# 0.01 written in exponential notation
0.01
>>> 1e4		# 10000 written in exponential notation
10000.0
>>> 4E-2	# E can also be used instead of e
0.04

What is the result of the following code?

>>> e
  1. 2.71828
  2. 100
  3. 10
  4. Raises NameError

The exponentiation notation requires it to be between two numbers. We can write in exponential notation only in the format {number}e{number}. Any other format will result in Python raising NameError.

Another type of literals is just used to define complex numbers.

Complex Numbers

A complex number is a number that can be expressed in the form a + bi, where a and b are real numbers, and i is a solution of the following equation.

$$
x = \sqrt{-1}
$$

Because no real number satisfies this equation, i is called an imaginary number or iota. In Python, we can use an imaginary number literal to create a complex number.

Imaginary Number Literals

An imaginary number literal yields a complex number with a real part of $0.0$. If a numeric literal contains $j$` or $J$ preceding a number, it is interpreted by the interpreter as a complex number.

>>> 1.5j
1.5j
>>> type(1.5j)
<class 'complex'>
>>> 3.14_15_93j
3.141593j

Python uses j or J instead of conventional i to represent iota.

What is the result of the following code?

>>> 100j**2
  1. 10000
  2. -10000
  3. (-10000+0j)
  4. Raises NameError

Literals form the basic building blocks of programming in the Python language.

Writing the Python interpreter's literal value creates an object with the literal value. Earlier, though, we mentioned that everything in Python is an object; we didn't detail what it means. Let's look into the concept of an object in Python next.

Objects

In Python, objects are abstractions for data.

All data in a Python program is represented by objects or by relations between objects.

In Python, an object is an entity that contains data along with associated attributes and methods. The object attributes give information about the object, while the object's functions are called its methods.

We will look at understanding more about object attributes and methods in the next sections. For now, the useful thing to know is that in Python, almost everything is an object. Keywords such as for, if are not objects but part of the Python language.

Every object has an identity, a type, and a value. Let's look at objects in Python in a bit more detail.

Object Identity

Once you create an object, Python assigns it a permanent identity.

In the first chapter, we saw that the computer stores data in the main memory with a corresponding memory address for the processor to locate the data. You may think of the object's identity as the object's address in memory, although it's not the same. The identity of an object is an integer. This integer is guaranteed to be unique and constant for this object during its lifetime.

We can check the identity of a created object using the id() function. The id() function produces a unique number identifying a specific value object in memory.

>>> id(12)
10914848				# Will be different for everyone
>>> id("Hello World")
140197441748912			# Will be different for everyone

Earlier, we used double equality (==) to check if two objects have the same value. You can use the is operator to check if two objects are identical or the same.

Two objects are said to be the same if they have the same id.

Let's understand using Python code.

>>> a = "Hello World"
>>> b = [a]
>>> b[0] is a
True
>>> id(b[0]) == id(a)
True

What is the result of the following code?

>>> pirates = ["Luffy", "Zorro"]
>>> guests = ["Luffy", "Zorro"]
>>> pirates is guests, pirates == guests
  1. (False True)
  2. (False True)
  3. (True True)
  4. (True False)

Object Type

The previous exercise mentions the names pirates, and guests refer to two different list-objects having the same value.

Every object has a specific type, which also determines the operations that the object supports. For example, an object with the type int supports division by another int type object, but an object with the type string doesn't.

The object's type also dictates which possible values the object can take.

The built-in function type() returns an object's type, which is an object itself. Like it's identity, an object's type is also unchangeable.

>>> type("Hello World")
<class 'str'>							# String
>>> type(121)
<class 'int'>							# Integer
>>> type(12.0)
<class 'float'>							# Float
>>> type(2j)
<class 'complex'>						# Complex

There are many types of objects available in Python. Integer, String, Float, Complex are part of the basic primitive data types. The type() function returns an object having the type type.

As we mentioned earlier, containers objects hold other references to other objects.

>>> number_list = [8, 31, 27, 29, 48, 56]
>>> type(number_list)
<class 'list'>					# List type Object
>>> type({"apples": 4, "bananas": 6})
<class 'dict'>
>>> type(('Rose', 'Lily', 'Sunflower'))
<class 'tuple'>

What is the output of the following code?

>>> type([1, 2, 3])
<class 'list'>
>>> type(_)
  1. <class 'type'>
  2. <class 'list'>
  3. Raises SyntaxError
  4. Raises TypeError

Object Value

The value of some objects can change. As we saw earlier, objects whose value can change are said to be mutable objects. At the same time, those objects whose value you cannot modify once you create them are called immutable. Its type determines an object's mutability. For instance, numbers, strings, and tuples are immutable, while dictionaries and lists are mutable. Figure 1 describes an object in Python.

Objects in Python
Figure 1: Object in Python

We know that strings are immutable objects whose value, id and type don't change once created. However, consider the following code:

>>> number = "Six hundred Sixty Six"
>>> number
'Six hundred Sixty Six'
>>> type(number)
<class 'str'>
>>> id(number)
139751547135152					# Will be different for everyone

We assigned the name number to the string object with the type str and a permanent id. Let's reassign the name number to another object.

>>> number = 666
>>> number
666
>>> type(number)
<class 'int'>					# Type changed
>>> id(number)
139751547506800					# Id changed

Here, type, id, and value of the name number changed.

The change of id, type, and value is because names in Python act as a reference to objects but are not objects themselves. Let's explore more about naming objects in the next section.

Identifiers

As we mentioned earlier, in Python, almost everything is an object. Literals are objects with constant values. When we assign a name to an object, we can refer to the object and its attributes using the name. However, the name itself is not the object. When you call the built-in function type() or id() on a name, it gives the type, or id of the object referred to by the name.

Why do you think we assign names to objects?

The primary purpose of assigning names to objects is to be able to reference them later. The name that we give to objects is called an identifier, variable, or merely name.

An identifier is a sequence of one or more characters used to name a given program element.

Naming an object

We name an object by writing the name we want to assign on the left-hand side, followed by the assignment operator (=) and the object we want to name.

>>> name = "Tom Sawyer"
>>> age = 12
>>> friends = ["Joe Harper", "Huckleberry Finn"]

Here we are naming the object Tom Sawyer as the name. In Python, names/identifiers don't have a type; objects do. The type of the name becomes the type of the object it references whenever we bind a name to an object.

>>> type(name)
<class 'str'>				# String
>>> type(age)
<class 'int'> 				# Integer
>>> type(friends)
<class 'list'>				# List

Let's try to assign a literal to another literal.

>>> 15 = 12
>>> "Tom Sawyer" = "Joe Harper"

In both cases, Python raises SyntaxError, informing it is impossible to assign to literal.

SyntaxError: can't assign to literal

Python considers numbers and strings literals to be objects with a constant value that cannot be modified and changed. When we assign 15 = 12, we wish to instruct Python to assign the name 15 to the number 12.

The use of a numeric literal as a name isn't allowed in Python.

When we earlier stated that strings and number literals are immutable or their value cannot be changed, this is what we meant.

The assignment operator is also used to modify mutable collection objects such as lists.

>>> a = [1,2,3]
>>> a[2] = 50
>>> a
[1, 2, 50]

When we access an object present inside a collection, it is similar to how an identifier references an object. Collection objects hold references to objects, not the object itself.

Let's do a small exercise to understand more about the object references.

What is the result of the following code?

>>> a = [1,2,3]
>>> b = [a]
>>> a = []
>>> print(b)
  1. [[1,2,3]]
  2. []
  3. [[]]
  4. Raises ValueError

In the previous exercise, when we assign a to the list b, what we are doing is assigning b[0] to the object referenced by a, i.e. [1, 2, 3].

>>> b = [a] 		# b = [[1,2,3]]

So, later on, when a is reassigned to another object, b[0] still references to the object [1, 2, 3]. The code in the exercise above might confuse you a bit. However, as we go along, it will all start to makes sense.

The way naming and referencing works in Python is important to know, especially if coming from other languages such as C. Let's check out how reassigning names work in Python.

Reassigning Identifiers

Once we assigned a name or an identifier to an object, we can reassign it to a different object.

>>> age = 14
>>> age
14						# type: integer
>>> age = 13.5
>>> age
13.5					# type: float
>>> age = "Thirteen years old"
>>> age
'Thirteen years old'	# type: string

In the above code, we assign the same name age to three distinct objects with different types. At each assignment, the name age ceases to refer to the previous object and refers to the new object assigned.

We can see that the name age can be re-assigned to different objects without restrictions. But what happens to the previous objects that the name used to refer to?

To understand what happens to previously referred objects, we need to look into object lifecycle and references. Let's look into it next.

Object Lifecycle

In any given program, objects are created, live for a while, and then destroyed.

The time between an object's creation (also known as instantiation or construction) until the object is no longer used and is destructed or freed is called the object life-cycle.

During a program, when an object is no longer required, it is removed. This process is called garbage collection. Depending on programming language, garbage collection can be automatic or manual. Python periodically frees and reclaims memory blocks that are no longer in use by automatically deleting unwanted objects.

Reference

Python implements garbage collection by storing the number of references of a given object called the reference count. When the reference count of a given object reaches zero, Python automatically removes the object.

To check the reference count of an object, we can use the getrefcount() function from the sys standard module available in Python.

Let's consider the following case.

>>> from sys import getrefcount
>>> book = "Alice in the world"		# A string object is created
>>> id(a)
140157471175352						# Will be different for everyone
>>> getrefcount(a)
2					# Initial Reference count of the string object

The name book is assigned to a string literal Alice in the wonderland. If we want to refer to this object, we can use the name book. Therefore, the name book is a reference to the string object Alice in the wonderland.

When you call the getrefcount() function with the argument book, the function returns the number of current references to the string object Alice in the wonderland.

The references show 2 instead, but we expected 1 that of the name book. Can you think of a reason you are getting this extra one reference?

The count returned is generally one higher than you might expect because it includes the object's temporary reference as an argument to the getrefcount() function.

When we use the object name inside a function invocation as an argument, it counts as a reference.

Therefore, the total number of references to the string object book is 2.

Let's increase our references to our string object by adding another name. We will assign the string object referenced by the name book to the name currently_reading.

>>> currently_reading = book # Assign the same object new name currently_reading

In the above code sample, when assigned currently_reading = book, it might seem that we created a copy of book and assigned it to currently_reading. That's not actually what's happening. When we assign currently_reading = book, we add another name currently_reading to the object referred to by book.

We can check that both the names, book, and currently_reading refer to the same object by checking their Ids or using the is operator.

>>> id(currently_reading)
140157471175352						# refers to the same object as `book`
>>> currently_reading is book		# is operator checks two objects are same
True

As we can see, both the name currently_reading and book are referencing the same string object. Now, let's check the reference count on the string object.

>>> getrefcount(book)
3									# Reference count increased

We can see that the reference count of the string object has increased. When we assign the name currently_reading to the name book, the string object gets a new name, currently_reading, increasing its reference.

Now, let's add another reference to the string object.

>>> reading_list = [currently_reading]    # Add to a list 													  # object`reading_list`
>>> reading_list
['Alice in the wonderland']

We included the string object's reference into a newly created list object with the name reading_list. Figure 2 shows the current references to the string object.

Figure 2: Three References of the String Object

We can check if the list object item references the same string object as the name book and currently_reading.

>>> id(reading_list[0]) # reading_list[0] refers to the same object
140157471175352
>>> reading_list[0] is book	# checks if two objects are same
True

In the code above, we can see that item in the reading_list list object references the same string object. Now let's check the reference count of the string object.

>>> getrefcount(reading_list)
4									# Reference count increased

As we expected, the reference count of the string object Alice in the world increases.

At this point, can you list all the references of the string object Alice in the wonderland?

Out of four references, three belongs to the names book, currently_reading, and reading_list[0].

The fourth one is a temporary reference due to passing the object to the getrefcount() function.

Deleting Names

The del keyword is used to delete statements in Python. The deletion of a name removes the binding of that name from the object it references to. If the name is unbound, a NameError exception is raised whenever you try to access the name.

Let's delete the name currently_reading.

>>> del currently_reading			# Delete the name currently_reading

We can verify by referring to the name in the console.

>>> currently_reading
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'currently_reading' is not defined

The del statement removes the name, not the object.

The object is removed automatically by Python's garbage collection if there are no references available to access it. Speaking of references, let's take a look at updated string object's reference in figure 3.

Figure 3: Two References to the String Object

Let's take a look at the effect of the deleted name currently_reading.

>>> book
'Alice in the wonderland'			# Can still access the object via `book`
>>> getrefcount(book)
3									# Reference count decreased
>>> reading_list
['Alice in the wonderland']			# deleting currently_reading doesn't affect

The string object Alice in the wonderland can be still be referenced via book and reading_list[0]. Let's re-assign the list reading_list to an empty list and check the book object's reference count.

>>> reading_list = []		# Assign reading_list to a blank list
>>> getrefcount(book)
2							# Reference count decreased
Figure 4: A single reference to the string object

The reference count again decreased when removed the reference from the list object reading_list. We can see the updated references in the figure 4.

When all the references of an object are gone, the object is automatically garbage collected by Python. If we delete the name book, we won't be able to refer to the string object. The final number of references is shown in the figure 5.

Figure 5: String object Deletion

Python increases the reference count for an object in the following ways:

  • When we assign an object to a name.
  • When we add an object to a container object. Such as appending to a list or adding as a property on a class instance.
  • When we pass the object as an argument to a function.

When we deleted the name currently_reading, using the statement del currently_reading, the list reading_list was unaffected.

Would things be different if we had deleted the name book with which we created the string object? Putting the question more general, if we delete the name with which an object is created, will other references to the object be affected?

Let's try to check.

Let's create an integer object and assign it the name a.

>>> from sys import getrefcount
>>> a = 786			# Create a integer object and name it `a`
>>> id(a)
10914944
>>> getrefcount(a)
2

Let's add a couple of more references to the same object.

>>> b = a
>>> c = [b]
>>> getrefcount(a)
4

We can verify that all three names a, b, and c refer to the same object using the id() function.

>>> id(a) = id(b) = id(c[0])
True

Now, let's delete the name a with which we first created the object.

>>> del a

Let's check if the b and c are still present.

>>> b		# Still Exists
786
>>> c		# Still Exists
[786]

We can see that the names b and c still retain their reference to the object.

This brings to an important point.

In Python, no particular name or reference owns the objects they refer to; the names always share the objects.

The sharing of the objects in Python becomes more important when we are dealing with mutable objects.

Can you take a guess why?

When different names share the mutable object, changes made via one name is reflected in all the names.

This is quite obvious because they refer to the same object.

Let's take a mutable list object instead of immutable strings and numbers to better understand this idea.

Let's assign a list object to the name a.

>>> a = [1, 2, 3]               # Create a list object and assign it to the name `a`

Now, let's add another name, b to the same list object.

>>> b = a						# Add another name to the list object
>>> b is a						# Check both b and a referring the same object
True

Now, let's change the content of the mutable list object using b.

>>> b[0] = 99					# Change the list object content using b
>>> b
[99, 2, 3]

Now, let's check the content of the list object referenced by a.

>>> a							# a also changes as same object is modified
[99, 2, 3]

We can see that the object was shared among the names a and b. The same mutable list object can be modified either by the name a or b.

When you modify mutable objects, it could sometimes result in some unwanted effect on your overall program. Therefore, do exercise caution while trying to work with mutable objects.

Interestingly, why do you think shared immutable objects don't have this issue?

Primarily because immutable objects cannot be modified. That's why they are called immutable in the first place.

Copies

Earlier, we saw that when using the expression b = a, it doesn't create a copy of the object. Instead, it adds a new reference to the object.

When we modify a mutable object through one of its references, the object's other references reflect the same modifications as they all point to the same object.

To avoid unwanted updates to an object, we can create a copy of an object rather than add a new reference.

A copy is sometimes needed, so one can change the copy without modifying the original object. Let's look into how we can copy objects in Python.

There are two types of copy operations to copy mutable container objects such as lists:

  • Shallow Copy
  • Deep Copy

Shallow Copy

We can perform a copy operation either by -

  • using the copy() function from a built-in module copy ,
  • or by creating a new container object with the same children objects.

Let's first use the copy() from the standard module to create a list object copy.

>>> from copy import copy
>>> a = [1, 2, 3]
>>> c = copy(a)					# Creating object using `copy`
>>> c
[1, 2, 3]
>>> c == a
True							# `c` Contains same elements as `a`
>>> c is a
False							# `c` refers to a different object

In the above code, we created a copy of the list object a, which is a different list object than a but contains the same elements as a. Now, let's modify c and see if it affects the list object a.

>>> c[2] = 10
>>> c
[1, 2, 10]				# `c` is modified
>>> a
[1, 2, 3]				# `a` is unchanged

As we can see, modifying a copy of the object doesn't affect the original object.

We can also copy a list object by copying the elements into a new list object. To do that, you might recall that the slice notation can be used to extract all the list elements.

Which of the following slice notation returns all the elements of the list object a.

  1. a[0:len(a)-1]
  2. a[0: -1]
  3. a[0::-1]
  4. a[:]

The slice notation a[:] returns all the list object's elements a[:]. Let's create a new list object with the same elements as a.

Let's create a list object a copy it's elements to d using slice notation.

>>> a = [1, 2, 3]
>>> a[:]				# Return all the elements
[1, 2, 3]
>>> d = a[:]			# `d` is a new list object with same elements as `a`
>>> d
[1, 2, 3]
>>> d is a				# `d` and `a` are two different objects
False

Let's see if modifying the original list object a affects the newly copied list object`.

>>> a[2] = 20			# Modified list object `a`
>>> a
[1, 2, 20]
>>> d
[1, 2, 3]				# List object `d` is unchanged

We can see that changing the original list object a does not affect the newly copied list object.

We saw two ways of copying the list object. However, both of these two instances of copying fail to explain why this is called shallow copying. Before I explain why this form of copying is called shallow, I would like to give you a hint to figure it out yourself. We are only copying the list, not the objects contained inside them. Now, would you like to guess as to why this is called shallow copying?

Let's figure out why this is called shallow copying.

We will need to take another instance but with slightly different elements.

Consider the copying operation of a nested list object such as x shown below.

>>> from copy import copy
>>> x = [[1, 2], [3, 4]]
>>> y = copy(x)				# Copy x
>>> y
[[1, 2], [3, 4]]
>>> y is x
False						# y and x are different objects

Now, let's modify the newly created list object 'y`.

>>> y[0][1] = 1000
>>> y
[[1, 1000], [3, 4]]				# y is changed
>>> x
[[1, 1000], [3, 4]]				# x is also changed

In this case, updating the copied object y results in x being modified as well. The reason for this is even though we are creating new container objects by copying, the elements in the container are still shared.

We can verify this by checking the elements of the two list-objects.

>>> y[1] is x[1]				# underlying object is shared
True

When we create a new copy using any list copying methods, Python shares the underlying objects while creating a new data-structure or container object. This form of copying is called shallow copying.

A shallow copy means constructing a new container object and then populating it with references to the original object's objects. It means that any changes made to a copy of the object reflect in the original object.
Figure 6: Shallow Copying

When the underlying collection objects in a collection are shared, it makes sense that modifying the underlying mutable object reflects in all the collections that hold the same object. Figure 6 shows shallow copying in Python.

We just covered how shallow copying can affect lists with mutable objects. To avoid this, we need to deep copy a collection object.

Can you guess how deep copying works?

Deep Copy

In a deep copy, Python recursively copies the collection object.

It first constructs a new collection object and then populates it with the copies of the children's objects of the original collection object rather than populating it with references.

Python's copy module has a deepcopy() function, which we can use to deep copy a collection.

Let's take a look at how it works.

>>> from copy import deepcopy
>>> a = [[1,2,3], [4,5,6]]
>>> b = deepcopy(a)					# Create a new list object
>>> b
[[1,2,3], [4,5,6]]
>>> b is a 							# `b` and `a` refers to two different objects
False
>>> b[0] is a[0]					# children objects are also different
False
>>> b[0][1] = 20					# Let's modify b
>>> b
[[1,20,3], [4,5,6]]
>>> a
[[1,2,3], [4,5,6]]					# `a` is not affected

In the above code sample, we can see that the deep copying of a collection object creates another collection object with the same values as the original object.

Figure 7: Deep Copying

When we modify a nested collection object of b, the original collection object referenced by a is not affected. Figure 7 describes deep copying in Python.

We have covered both deep copying and shallow copying. When does deep copying collection make sense?

When you have a collection object with mutable elements, and you would like to protect them from unwanted or accidental updates, deep copying is the way to go.

In the next section, let's look into some rules and conventions to name objects.

Identifiers

An identifier is assigned to objects and expressions in Python primarily to refer to them later.

Python also uses a set of identifiers or reserved words that have predefined meanings called keywords. The following are the keywords that Python uses for its internal purpose.

False               def                 if                  raise
None                del                 import              return
True                elif                in                  try
and                 else                is                  while
as                  except              lambda              with
assert              finally             nonlocal            yield
break               for                 not
class               from                or
continue            global              pass

There are specific rules that we need to follow to write an identifier in Python.

  1. Identifiers may contain letters and digits, but can not begin with a digit. For example, 3apples is an invalid identifier, while apples3 is valid.
  2. The underscore character _ is also allowed to aid in the readability of long identifier names. For example, number_of_daily_customers is easy to read.
  3. We cannot use keywords as identifiers.
  4. We cannot use special symbols such as ! @, #, $, %,^, or * used in an identifier.
  5. The identifier can be of any length.
  6. Spaces are not allowed as part of an identifier.

In Python, we can store the reference to objects using identifiers. To work with the objects, Python has a list of built-in operators that we will look into in the next section.

Operators

Python provides the following types of built-in operators.

Table 3: Built-in Operators in Python
Name Operators
Arithmetic Operators +, -, *, /
Assignment Operator =
Comparison Operators <, >, ==
Logical Operators or Boolean Operators and, or, not
Identity and Membership Operators is, is not, in, not in
Before we start, let's revisit the definition of an operator. Can you recall what an operator is?

Earlier, we defined operator, which are symbols that represent a specific operation.

We can state that operators in Python are symbols that instruct the Python interpreter to perform specific mathematical or logical manipulations.

Let's start with the Arithmetic operator.

Arithmetic Operator

Earlier, we have played around with the arithmetic operators in the quick tour of the Python section. Table 2 shows the list of arithmetic operators available in Python.

Table 4: Arithmetic Operators
Operator Name Description
a + b Addition Sum of a and b
a - b Subtraction Difference of a and b
a * b Multiplication Product of a & b
a / b Division Quotient of a by b
a // b Floor Division Quotient ofa and b, removing decimal parts
a % b Modulus Remainder after division of a by b
a ** b Exponentiation a raised to the power of b

In the code sample below, we can see how arithmetic operators work.

>>> 4 + 5						# Addition
9
>>> 4 - 1						# Subtraction
3
>>> 4 * 2						# Multiplication
8
>>> 11 / 4						# True Division
2.75
>>> 11 // 4						# Floor Division
2
>>> 11 % 4						# Modulus
3
>>> 2 ** 4						# Exponentiation
16
>>> ( ((1*2) + 3) ** 4) / 4 - 5 # Using parentheses to group elements
151.25

What is the value of the following expression in Python?

>>> a, b, c = 12, 9, 3
>>> ((a + b**c) + (a**c + a))//b
  1. 275
  2. 375
  3. 475
  4. 575

Assignment Operator

The assignment operator (=) is used to (re)bind names to objects and to modify attributes or items of mutable objects.

We have earlier (re)assigned names to objects.

>>> name = "Primer"			# Bind `name` to the `str` object
>>> age = 1000				# Bind  `age` to the `int` object
>>> name = "Primerlabs"		# Rebind name to the 'str' object

We can assign multiple names to various objects simultaneously.

>>> a, b, c = 10, 20, 30
>>> print(a, b, c)
10 20 30

We can also swap two names to interchange their object reference.

>>> country, captial = "Khartoum", "Sudan"
>>> print(country, capital)
Khartoum Sudan								# Ooops. Got it wrong.
>>> country, capital = capital, country		# Let's swap the variables
>>> print(country, capital)
Sudan Khartoum								# Fixed it. :)

Although multiple assignments seem a simultaneous operation, the assignment occurs left-to-right, i.e., the left name is updated first, and then the right name is updated. The simultaneous update can confuse the programmer. The following code sample explains the same.

>>> x = [10, 20]
>>> i = 0
>>> i, x[i] = 1, 40		# Left is updated first, then right
>>> x
[10, 40]				# Expected [40, 20]

Had Python simultaneously updated the object, the object referenced by x would have been changed to [40, 20].

We saw earlier that we could use the names with arithmetic operations for more straightforward calculations.

>>> hours_in_a_day = 24
>>> days_in_a_week = 7
>>> hours_in_a_week = hours_in_a_day * days_in_a_week
>>> print(hours_in_a_week)
168

We can also update the objects by using operators on their value.

>>> age = 13
>>> age = age + 1
>>> print(age)
14
>>> age = age - 5
>>> print(age)
9

This type of combined operation, where we are updating the object's value and assigning it to a new value, is reasonably common in programming. The combined operation is so common that Python has a built-in update operator for all arithmetic operations.

>>> age = 13
>>> age += 1 	# 14
>>> age -= 5 	# 9
>>> age /= 4	# 2.25
>>> age *= 16  	# 36.0
>>> age	//= 3	# 12.0
>>> age **= 2	# 144.0

These built-in operators act as shorthand for their own expanded version, as shown in the table 3.

Table 3: Combined Operations
Combined Operation Expanded Operation
a += b a = a + b
a -= b a = a - b
a *= b a = a * b
a /= b a = a / b
a //= b a = a // b
a %= b a = a % b
a **= b a = a ** b

What is the value of a at the end of the following code?

>>> a, b = 10, 20
>>> b -= a
>>> b //= 4
>>> b **= a
>>> b *= 7
>>> b += 14
>>> b
  1. 1024
  2. 49000
  3. 7182
  4. 10

For the previous exercise, sometimes it pays off to read the question properly. Next, we will look into comparison operators in Python.

Comparison Operators

Usually, when you make a comparison statement, the statement can either be True or False.

Can a comparison statement be neither True nor False?

A comparison statement can be neither True nor False happen when we compare two objects which cannot be compared, such as "An apple is greater than the color green". This is an absurd statement with no way of comparing these two objects. We can say that the comparison is not supported between these two objects. A comparison in Python works similarly. Let's look at comparison operators in detail.

A Boolean value is either true or false.

In Python, the two Boolean values are True and False.

A Boolean expression is one that evaluates to produce a result that is either True or False.

Comparison operators are used to writing Boolean expressions.

Table 4 shows some of the comparison operators that Python provides.

Table 4: Comparision Operators
Operation Meaning
< strictly less than
<= less than or equal
> strictly greater than
>= greater than or equal
== equal
!= not equal

For numbers, this is quite straightforward:

>>> x, y = 10, -12
>>> x > y
True
>>> z = 5*2
>>> x == z
True
>>> y >= 5
False

We can also use the comparison operators with strings.

>>> name = "Primer"
>>> "Pri" + "mer" == name
True
What do you think the result of the following Python comparison statement would return?
>>> 'Python' > 'javascript'

The answer is True.

>>> 'Python' > 'javascript'
True

When you use ( > , < , <= , <= , == , != ) to compare two strings, Python compares two strings lexicographically.

Lexiocographic ordering

The lexicographic or lexicographical order is the ordering of words based on the alphabetical order of their component letters.

The way Python compares is by comparing the Unicode code point of each character of the string. Python provides a built-in function ord() to get the Unicode code point of a single string character.

>>> ord('a')
97
>>> ord('A')
65
>>> ord('[')
91
>>> ord('1')
49

To retrieve the character from a Unicode code point, you can use the built-in function chr().

>>> chr(97)
'a'
>>> chr(65)
'A'
>>> chr(91)
'['
>>> chr(49)
'1'

When Python compares two strings, it sequentially compares each of their corresponding characters.

>>> string_1 = 'Mantle'
>>> string_2 = 'Mandate'
>>> string_1 > string_2
True

In the above example, Python first compares the first character, M. As they are equal, it proceeds to the next character a, which is again equal, and the same goes for the third character n. The fourth character is the tiebreaker for the string as t has a higher Unicode code point. Therefore the literal string Mantle is higher lexicographically than the literal Mandate.

>>> ord('t')
116
>>> ord('d')
100
You can remember this type of comparison as the way how an English dictionary arranges the words.
What do you think happens when you execute the following code?
>>> "Hello World" > 0.5

This is one example out of many when the comparison is not defined. If you try out in the interpreter, you will find Python raising TypeError with the following message.

TypeError:'>' not supported between instances of 'str' and 'float'

However, you can still check for equality or inequality between these two types of objects.

The next type of operator in our list is a Logical Operator.

But before going into that, we need to understand the truth value first.

Truth Value Testing

In Python, we can test any object for its truth value using the built-in bool() function. The truth value of any object is either True or False.

>>> bool(1231)
True
>>> bool("Hello World")
True
>>> bool([1,2,3])
True

By default all objects are considered to have Boolean value True with exception to the following few built-in objects:

  • Python constants which are defined to be false: None and False
  • zero of any numeric literal: 0, 0.0, Decimal(0), Fraction(0, 1)
  • empty sequences and collections: '', (), {}, set(), range(0)
>>> bool(None)
False
>>> bool(0.0)
False
>>> a = ""		# Empty String
>>> bool(a)
False
>>> b = []		# Empty List
>>> bool(b)
False

The if statement in Python checks if the condition provided is either True or False. We can test any object for its truth value. Therefore we can provide any object as a condition to an if statement.

Objects are tested for their truth values mainly at control flow statements such as if and while conditions. Earlier in the sample script, we wrote a conditional statement to check if a given number is even.

Python executes the code block inside an if statement, only it finds the when the condition(s) to be True.

>>> x = 24
>>> if x % 2 == 0:				# condition
...    	print("x is even")
x is even

Python evaluates the condition used in the if statement to return either True or False. Thus, it checks the truth value of the expression.

>>> x = 24
>>> bool(24 % 2 == 0)
True

Which of the following has the truth value of True in Python?

  1. []
  2. 0.00
  3. False
  4. "A"

Boolean Operators

We can form complex Boolean expressions by using Boolean operators, also known as logical operators. In Python, the Boolean operators are or, and and not. Table 5 shows the Boolean operations ordered by descending priority.

Table 5: Boolean Operators
Operation Result Priority
x or y if x is False, then y, else x 1
x and y if x is False, then x, else y 2
not x if x is false, then True, else False 3

The evaluation using the and or operators follow these rules:

  • In the expression x and y, Python first evaluates x. If x is false, Python returns its value. Otherwise, Python evaluates y and returns the resulting value.
  • In the expression x or y, Python first evaluates x. If x is true, Python returns its value. Otherwise, Python evaluates y and returns the resulting value.
  • The operator not yields True if its argument is false, False otherwise.

What is the result of the following code output?

>>> [] or [1, 2, 3]
  1. []
  2. [1, 2, 3]
  3. True
  4. False

In the expression [] or [1, 2, 3] in the above exercise, Python first evaluates []. [] is an empty list with the truth value False. Therefore Python returns [1, 2, 3].

What is the result of the following code?

>>> 0.00 and 5.00
  1. 0.00
  2. 5.00
  3. False
  4. True

In the expression 0.00 and 5.00, Python first evaluates 0.00. As 0.00 is False, Python returns its value; otherwise, Python would have evaluated 5.00 and return 5.00.

>>> given_numbers = []
>>> print(given_numbers or 'No numbers provided')

What is the output of the code above?

>>> given_numbers = []
>>> print(given_numbers or 'No numbers provided')
  1. []
  2. No numbers provided
  3. False
  4. True

As an empty list object [] is False, Python returns the second value, therefore, Python returns No numbers provided.

What about the following code?

>>> given_numbers = [1, 2]
>>> print(given_numbers or 'No numbers provided')
  1. [1, 2]
  2. No numbers provided
  3. False
  4. True

Since the first object is a non-empty list [1, 2], Python returns the list object.

Python doesn't evaluate the second item at all. Let's take another exercise to use compound Boolean expression using logical operators.

The keyword in is a membership operator that checks if a collection has a particular item. We can form a compound Boolean expression by using logical operators.

What do you think is the result of the following code output?

>>> guests = ['Luffy', 'Zorro', 'Sanji']  # Length = 3
>>> if len(guests) > 4 or  'Luffy' in guests:
...  	print("Open the doors")
  1. 'Open the doors'
  2. True
  3. ['Luffy', 'Zorro', 'Sanji']
  4. False

The condition len(guests) > 4 doesn't evaluate to True while the condition ``'Luffy' in guestsevaluates toTrue`.

Therefore, the if condition is met, Python executes the code block inside it, printing Open the doors.

What about the following code?

>>> guests = ['Luffy', 'Zorro', 'Sanji']
>>> if len(guests) > 4 and 'Luffy' in guests:
...		print("Open the doors")
  1. 'Open the doors'
  2. True
  3. ['Luffy', 'Zorro', 'Sanji']
  4. Nothing happens

The print() function inside the if code block is not executed in the above code.

In the compound Boolean condition len(guests) > 4 and 'Luffy' in guests, both the conditions need to be true for Python to execute the code block.

Therefore the print() function is not executed.

Boolean and Comparison Operator

We can chain Boolean expressions using logical operators in multiple ways—for instance, the following compound statements.

>>> age = 15
>>> 13 <= age < 21	# True
>>> 15 < age <= 21	# False

The above code sample is correct in Python because Python expands the expression into the following code :

>>> age = 15
>>> 13 < age < 21
>>> 13 < age and age < 21

What is the output of the below code?

>>> guests = ['Luffy', 'Zorro']
>>> if 2 < len(guests) < 4:
...     print("Serve a pizza")
... elif len(guests) <= 2:
...     print("Serve two tacos")
... else:
...     print("Too many guests")
  1. Serve a pizza
  2. Serve two tacos
  3. Too many guests
  4. Returns nothing

Like and, or, and not, Python also contains intuitive operators we can use to check for identity and membership. Let's look at identity operators next.

Let's look into identity and membership operator next.

Identity and Membership Operator

Table 6 shows the identity and membership operators in Python.

Table 6: Identity Operators
Operator Description
a is b True if a and b are identical objects
a is not b True if a and b are not identical objects
a in b True if a is a member of b
a not in b True if a is not a member of b

The identity operators is and is not, check for object identity. As we mentioned earlier, comparing the identities of the two objects is different than checking their equality. Objects can have equal value and still have a different identity. Let's understand using code examples.

>>> x = [21, 22, 42]
>>> y = [21, 22, 42]
>>> x == y		# True
>>> x is y		# False
>>> x is not b 	# True

What do identical objects look like? Here is an example:

>>> x = [21, 22, 42]
>>> y = x
>>> x is y 		# True
>>> id(x) == id(y)
True

The difference between the two cases here is that in the first, a and b point to different objects, while in the second, they point to the same object. As we saw in the previous section, Python identifiers are references to the object.

The is operator checks whether the two names point to the same container object rather than referring to what the container contains.

What is the result of the following code?

>>> a = [1, 2, 3]
>>> b = [a]
>>> b[0] is a
  1. True
  2. False

In the previous exercise, b is a list object that contains the references to the object a at position 0. Therefore, both b[0] and the name a refer to the same list object.

Hence b[0] is a evluates to True.

Membership Operators

Container objects hold references to other objects. The objects they hold are sometimes referred to as the members of the object. Membership operators check for membership within container objects.

>>> 2 in [1,2,4,6]				# Is 2 present in the list?
True
>>>	3 not in [3, 4, 5]			# Is 3 not present in the list?
False
>>> 'good' in 'good year'	# Is there is any `good` in `good year`
True

As you can notice, checking for membership is pretty straightforward and intuitive in Python.

What is the output of the following code?

>>> (1 in [3, [1]], [2] in [2])
  1. (False, False)
  2. (True, True)
  3. (True, False)
  4. (False, True)

We can come to the end of this chapter. In the next chapter, we will look into one of the essential parts of a programming language: Functions.


[1]: Numbers that are divisible by two are called even numbers while those which are not are divisible by 2 are odd numbers. ↩︎