A Not-so-Quick-but-Conceptual guide to Python — Notebook | Intermediate | Part 1

A Not-so-Quick-but-Conceptual guide to Python — Notebook | Intermediate | Part 1

19/12/2018 Off By Tamoghna Saha

Hi. As mentioned in my previous article, I am now sharing the intermediate part of the Python Tutorial series. I decided to split this part up into 2. Part 1 covers:
1. Data Types [list, tuple, dictionary, set, frozenset]
2. Conditional statements and loops [if-elif-else, for, while, break, continue]
3. File IO [read, write, append, close]
4. Exception Handling [try/except, finally, assertion]

More Data Types

List of immutable objects:

  • bool
  • int
  • float
  • str
  • tuple
  • frozenset

List of mutable objects:

  • list
  • set
  • dict


List is a type of mutable object in Python used to store an indexed list of item. It is created using square brackets with commas separating the items. A certain item from the list can be accessed using it’s index number.

An empty list is created using an empty pair of square brackets.

A list can contain items of a single item type or multiple item type. A list within a list is also possible which is used to represent 2D grids as Python, by default, lacks multi-dimensional array feature. But this can be achieved using numpy which we will discuss in our Advanced Notebook.

NOTE: Strings can be indexed like a list.

>>> empty_list = []
>>> words = ['Python',3.6,['Up Next','Tuple']]

>>> print(words[0])
>>> print(words[2][1])

>>> print(words[1])
>>> words[1]=2.7
>>> print(words)
## accessing items of string by index
>>> short_form = "User Defined Function"
>>> print("In our last class, we went through {}{}{}".format(short_form[0], short_form[5], short_form[13]))
['Python', 2.7, ['Up Next', 'Tuple']]
In our last class, we went through UDF
## basic list operations
>>> num_list = [1,2,3]
>>> print(num_list + [4,5,6])
>>> print(num_list * 2)
>>> print(4 in num_list)
>>> print(9 not in num_list)
>>> print(not 1 in num_list)
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 1, 2, 3]
# list functions and methods
>>> num = [1,2,3]
>>> num.append(4)
>>> num.append([5,6])
>>> print(num)
>>> print(len(num))

>>> num.extend([7,8,9])
>>> print(num)
>>> print(len(num))

>>> index = 2
>>> num.insert(index, 2.5)
>>> print(num)
>>> print("-----------------------------")
>>> print(num.index(3))
>>> print(num.index(2.0))

>>> num.remove([5,6])
>>> num.pop()
>>> print(num)

>>> print(max(num))
>>> print(num.count(5))

>>> num.reverse()
>>> print(num)
[1, 2, 3, 4, [5, 6]]
[1, 2, 3, 4, [5, 6], 7, 8, 9]
[1, 2, 2.5, 3, 4, [5, 6], 7, 8, 9]
[1, 2, 2.5, 3, 4, 7, 8]
[8, 7, 4, 3, 2.5, 2, 1]


Tuple is a immutable Python object created using Parentheses with commas separating the items which can be accessed using it’s index number.

It is just like list, except you cannot modify or re-assign a value in the tuple. It will throw TypeError.

An empty tuple can also be created in the same way you create empty list.

NOTE: Tuples can also be created without using Parentheses, simply separated by commas.

But when should we put parenthesis on tuples, and when we shouldn’t?

Answer: We use parentheses for nested tuple.

>>> empty_tuple = ()

>>> my_tuple = ("is", "my", "tuple")
>>> print(my_tuple[2])

>>> also_my_tuple = 'is', 'this', 'one'
>>> print(also_my_tuple[3])

IndexError Traceback (most recent call last)
<ipython-input-5-7a7df2b84a95> in <module>
6 also_my_tuple = 'is', 'this', 'one'
----> 7 print(also_my_tuple[3])

IndexError: tuple index out of range


Dictionaries are data structures used to map keys to values. Just like list, they are mutable and made using curly brackets.

Dictionaries can be indexed in the same way as lists, using square brackets containing keys. Trying to index a key that isn’t a part of the dictionary returns a KeyError.

The useful dictionary method is get. It does same thing as indexing, but if the key is not found in the dictionary, it returns None instead of throwing any error.

NOTE: Only immutable objects can be used as keys to dictionaries.

>>> empty_dict = {}

>>> my_dict = {"Joker":'Why so serious?', "Bane": [1,2,3], "Scarecrow": 0.05}
>>> print(my_dict["Joker"])

>>> my_dict_again = {(1,2,3):[1,2,3], ('a','b','c'):['a','b','c']}
>>> print(my_dict_again[(1,2,3)])

>>> my_dict_again[4] = 'd'
>>> print(my_dict_again)
>>> print(empty_dict[0])
Why so serious?
[1, 2, 3]
{(1, 2, 3): [1, 2, 3], ('a', 'b', 'c'): ['a', 'b', 'c'], 4: 'd'}

KeyError Traceback (most recent call last)
<ipython-input-6-75e3176fb6bc> in <module>
10 print(my_dict_again)
---> 12 print(empty_dict[0])

KeyError: 0

Now see this.

>>> my_dict_error = {[1,2,3]:'a',}
TypeError Traceback (most recent call last)
<ipython-input-8-859c69ec502a> in <module>
----> 1 my_dict_error = {[1,2,3]:'a',}

TypeError: unhashable type: 'list'

What does this error mean?

An object is hashable if it has a hash value which never changes during its lifetime (it needs hash() method). List is unhashable because it’s content can change over its lifetime.

NOTE: Don’t know about Hash function? Think about it as the fingerprint of a file in an encrypted format. For details, search in Google.

# How about a dictionary within a dictionary?
>>> my_dict_yet_again = {0:"infinity", 1:{'a':[1,2]}, 'b':{2: (3,4)}}
>>> print(my_dict_yet_again)
>>> print(my_dict_yet_again[1]['a'])

# Some dictionary functions
>>> print(1 in my_dict_yet_again) # check if a key is available in the dictionary
>>> print({'a':[1,2]} in my_dict_yet_again.values()) #check if the value is available in the dictionary

>>> my_dict_yet_again[True]=False
>>> print(my_dict_yet_again)

>>> print(my_dict_yet_again.get(1)) # get method
>>> print(my_dict_yet_again.get(2))
>>> print(my_dict_yet_again.get("True","True string is not available as key"))
{0: 'infinity', 1: {'a': [1, 2]}, 'b': {2: (3, 4)}}
[1, 2]
{0: 'infinity', 1: False, 'b': {2: (3, 4)}}
True string is not available as key

Set and Frozenset

Sets are Python object similar to lists or dictionaries. They are created using curly braces or the set function. They are unordered, which means that they can’t be indexed. They cannot contain duplicate elements. Due to the way they’re stored, it’s faster to check whether an item is part of a set, rather than part of a list.

Instead of using append to add an item to a set, we use add. The method remove removes a specific element from a set but pop removes an arbitrary element.

>>> my_set = set((2.7,"3.6"))
>>> also_my_set = {2.7, 3.6}
>>> my_frozenset = frozenset(("Python","2.7"))

>>> my_set.add(6)
>>> print(my_set)
>>> print(my_frozenset)

>>> my_set.remove(2.7)
>>> my_set.pop()
>>> print(my_set)
# some additional operations
>>> first = {1, 2, 3, 4, 5, 6}
>>> second = {4, 5, 6, 7, 8, 9}
>>> print(first | second)
>>> print(first & second)
>>> print(first - second)
>>> print(second - first)
>>> print(first ^ second)
{'3.6', 2.7, 6}
frozenset({'2.7', 'Python'})
{1, 2, 3, 4, 5, 6, 7, 8, 9}
{4, 5, 6}
{1, 2, 3}
{8, 9, 7}
{1, 2, 3, 7, 8, 9}

So, what are the operations that can be performed on mutable and immutable objects in Python?

Please check the answer in the notebook attached below.

Conditional statements, loops

if, elif, else

Python uses if statements to run code if a certain condition holds True, otherwise they aren’t.

NOTE: Python uses indentation (white space at the beginning of a line) to delimit blocks of code. Other languages, such as C, use curly braces to accomplish this, but in Python, indentation is mandatory; programs won’t work without it.

To perform more complex checks, if statements can be nested, one inside the other. This means that the inner if statement is the statement part of the outer one.

An else statement follows an if statement, and contains code that is called when the if statement evaluates to False.

The elif (short for else if) statement is a shortcut to use when chaining if and else statements. A series of if elif statements can have a final else block, which is called if none of the if or elif expressions is True.

>>> num = int(input("Enter a number from 0 to 9: "))
>>> if num == 3:
print("You got 3")
elif num == 6:
print("Number is 6")
elif num == 9:
print("Looks like 9")
print("`If you knew the magnificence of the three, six and nine, you would have a key to the universe.` ~ Tesla")
Enter a number from 0 to 9: 5
`If you knew the magnificence of the three, six and nine, you would have a key to the universe.` ~ Tesla


The for loop is commonly used to repeat some code a certain number of times on an object known as iterator. This is done by combining for loops with range objects.

The function range by default starts counting from 0 and goes up to the n-th value i.e., till (n-1). Its first parameter is the starting point, second one is the ending point (n-th value), and the optional third is the step value.

>>> for i in range(5):
if i < 3:

>>> print("-----------------")

>>> for i in range(2,12,3):

Two questions for you guys

  1. Ever wondered why people use i in for loop?
  2. What is the difference between range(x) and list(range(x))?
>>> print(list(range(5)))
>>> print(range(5))
[0, 1, 2, 3, 4]
range(0, 5)


  1. The i stands for the item to be accessed from any iterable or range object.
  2. When you execute ?range, it will say that range returns an object that produces a sequence of integers from start (inclusive) to stop (exclusive) by step without assigning indexes to the values. Rather, it generates only one number at a time, relying on a for loop to request for the next item in the range to be seen. However, list(range()) does assign indices and hence allows you to see the full sequence of the numbers immediately, without the assistance of a for loop. By the way, this happens only in Python 3.


An if statement is run once if its condition evaluates to True. A while statement is similar, except that it can be run more than once. The statements inside it are repeatedly executed, as long as the condition holds True. Once it evaluates to False, the next section of code is executed.

The infinite loop is a special kind of while loop where the condition is always True and never stops iterating.

To end a while loop prematurely, the break statement can be used inside the loop.

When a continue statement is encountered, the code flow jumps back to the top of the loop, rather than stopping it completely (which is the job of break). Basically it stops the current iteration and continues with the next one.

>>> import time
>>> mewtwo_cp = 30
>>> umbreon_dark_pulse = 3
>>> fight = True
>>> print("You attacked Mewtwo! CP left: {}".format(mewtwo_cp))

>>> while fight:
print("Umbreon used Dark Pulse!\n")
mewtwo_cp = mewtwo_cp - umbreon_dark_pulse

if mewtwo_cp == 15:
print("He's halfway dead!")
elif mewtwo_cp == 9:
print("Take him down!")
elif mewtwo_cp == 3:
print("Now's your chance")

if mewtwo_cp <= 0:

print("Mewtwo CP left: {}".format(mewtwo_cp))

>>> print("Congrats! Your Umbreon won.")
You attacked Mewtwo! CP left: 30
Umbreon used Dark Pulse!

Mewtwo CP left: 27
Umbreon used Dark Pulse!

Mewtwo CP left: 24
Umbreon used Dark Pulse!

Mewtwo CP left: 21
Umbreon used Dark Pulse!

Mewtwo CP left: 18
Umbreon used Dark Pulse!

He's halfway dead!
Umbreon used Dark Pulse!

Mewtwo CP left: 12
Umbreon used Dark Pulse!

Take him down!
Umbreon used Dark Pulse!

Mewtwo CP left: 6
Umbreon used Dark Pulse!

Now's your chance
Umbreon used Dark Pulse!

Congrats! Your Umbreon won.

File I/O

Python can be used to read and write the contents of files. Text files are the easiest to manipulate.


Before a file can be edited, it must be opened, using the open function. The argument of the open function is the path to the file. If the file is in the current working directory of the program, you can specify only its name.


There are mode used to open a file by applying a second argument to the open function.

  • “r” means open in read mode, which is the default.
  • “w” means write mode, for rewriting the contents of a file.
  • “a” means append mode, for adding new content to the end of the file.
  • “b” means binary mode, which is used for non-text files (such as image and sound files).


The contents of a file that has been opened in text mode can be read using the read method. To read only a certain amount of a file, you can provide a number as an argument to the read function. This determines the number of bytes that should be read.

After all contents in a file have been read, any attempts to read further from that file will return an empty string, because you are trying to read from the end of the file.

To retrieve each line in a file, you can use the readlines method to return a list in which each element is a line in the file.

NOTE: There is a readline and a readlines method. readline() reads one line character at a time, readlines() reads in the whole file at once and splits it by line.


To write to files we use the write method, which writes a string to the file. The “w” mode will create a file, if it does not already exist. When a file is opened in write mode, the file’s existing content is deleted. The write method returns the number of bytes written to a file, if successful.

NOTE: If you need to write anything other than string on a file, it has to be converted to a string first.


Once a file has been opened and used, it should be closed which is done with the close method of the file object.

Alternative approach of file access

An alternative way of doing it is using with statements. This creates a temporary variable (often called f), which is only accessible in the indented block of the with statement. The file is automatically closed at the end of the with statement, even if exceptions occur within it.

>>> file_name = open("def_NN.txt", "r")
>>> print("------- Reading the content -------\n")
>>> file_content = file_name.read()
>>> print(file_content)
>>> print("------- Re-reading -------")
>>> print(file_name.read())
>>> print("------- Finished! --------\n")
>>> print("------- Closing the file -------")
>>> file_name.close()
# try readlines
------- Reading the content -------

What is Neural Network?
A neural network is a processing unit that is capable to store knowledge and apply it to make predictions. A neural network mimics the brain in a way where the network acquires knowledge from its environment through a learning process. Then, intervention connection strengths known as synaptic weights are used to store the acquired knowledge. In the learning process, the synaptic weights of the network are modified in such a way so as to attain the desired objective.

A neural network architecture comprises of 3 types of layers:

Input Layer: The first layer in the network which receives input (training observations) and passed to the next hidden layer(s)
Hidden Layer: The intermediate processing layer(s) which perform specific tasks on the incoming data and pass on the output generated by them to the next output layer
Output Layer: The final layer of the network which generates the desired output
Each of these layers are composed of Perceptrons which is analogous to the neuron of the our nervous system.

------- Re-reading -------

------- Finished! --------

------- Closing the file -------
>>> file_name = open("joker.txt", "r")
>>> print("------- Reading initial contents ------- \n")
>>> print(file_name.read())
>>> print("------- Finished ------- \n")
>>> file_name.close()
>>> file_name = open("joker.txt", "w")
>>> amount_written = file_name.write("I believe whatever doesn't kill you simply makes you...stranger.")
>>> print("Amount of text written: {}\n".format(amount_written))
>>> file_name.close()
>>> file_name = open("joker.txt", "r")
>>> print("------- Reading new contents ------- \n")
>>> print(file_name.read())
>>> print("\n ------- Finished -------")
>>> file_name.close()
------- Reading initial contents ------

You see, in their last moment, people show you who they really are.

------- Finished -------

Amount of text written: 64

------- Reading new contents -------

I believe whatever doesn't kill you simply makes you...stranger.

------- Finished -------
# alternative approach
>>> with open("def_NN.txt") as f:

Exception Handling


Exception occur when something goes wrong due to incorrect code syntax or logic or input. When an exception occurs, the program immediately stops and doesn’t executes any lines further.

Different exceptions are raised for different reasons. Some common exceptions are listed below:

  • ImportError: an import fails
  • IndexError: a list is indexed with an out-of-range number
  • NameError: an unknown variable is used
  • SyntaxError: the code can’t be parsed or processed properly
  • TypeError: a function is called on a value of an inappropriate type
  • ValueError: a function is called on a value of the correct type, but with an inappropriate value

Third-party libraries and modules define their own exceptions. Learn more about built-in exceptions here

Here are some examples of different built-in exceptions.

>>> list=[1,2,3] 
>>> print(list[3])
Traceback (most recent call last): File "<stdin>", line 1, in <module> 
IndexError: list index out of range 

>>> printf(a)
File "<stdin>", line 1 printf a     
SyntaxError: invalid syntax

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

>>> import tk Traceback 
(most recent call last): File "<stdin>", line 1, in <module>
ImportError: No module named tk 

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

>>> list.remove(0) 
Traceback (most recent call last): File "<stdin>", line 1, in <module> 
ValueError: list.remove(x): x not in list

Exception Handling

To handle exceptions and call code when an exception occurs, we have to use a try/except statement.


The try block contains code that might throw an exception. If that exception occurs, the code in the try block stops executing, and the code in the except block is run. If no error occurs, the code in the except block doesn’t run.

A try statement can have multiple different except blocks to handle different exceptions. Multiple exceptions can also be put into a single except block using parentheses, to have the except block handle all of them.

An except statement without any exception specified will catch all errors. However, this kind of coding should be avoided. If you do this, you are going against the zen of Python.

Exception handling is particularly useful when

  • dealing with user input
  • sending stuff over network or saving large amounts of data, since issues happening with hardware like losing power or signal problems can happen
>>> try:
variable = 10
print(variable + "hello")
num1 = 7
num2 = 0
print(num1 / num2)
print("Done calculation")
except ZeroDivisionError:
print("An error occurred due to zero division")
except (ValueError, TypeError):


We can use raise to throw an exception if a condition occurs. The statement can be complemented with a custom exception.

>>> x = 12
>>> if x > 5:
raise Exception('x should not exceed 5. The value of x was: {}'.format(x))
Exception Traceback (most recent call last)
<ipython-input-5-b2de8492a496> in <module>
1 x = 12
2 if x > 5:
----> 3 raise Exception('x should not exceed 5. The value of x was: {}'.format(x))

Exception: x should not exceed 5. The value of x was: 12

else and finally

Here using the else statement, you can instruct a program to execute a certain block of code only in the absence of exceptions.

To ensure some code runs no matter what errors occur, you can use a finally statement. The finally statement is placed at the bottom of a try/except statement and else statement, if any. This diagram explains the concept of try-except-else-finally.

>>> try:
num_1 = 2
num_2 = 5
except ZeroDivisionError as error:
with open('joker.txt') as file:
read_data = file.readline()
except FileNotFoundError as fnf_error:
print('Getting printed irrespective of any exceptions.')
I believe whatever doesn't kill you simply makes you...stranger.
Getting printed irrespective of any exceptions.
>>> try:
print(10 / 0)
except ZeroDivisionError:
print("This is executed last!")
This is executed last!

ZeroDivisionError Traceback (most recent call last)
<ipython-input-7-b26411555290> in <module>
2 print(1)
----> 3 print(10 / 0)
4 except ZeroDivisionError:

ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

NameError Traceback (most recent call last)
<ipython-input-7-b26411555290> in <module>
3 print(10 / 0)
4 except ZeroDivisionError:
----> 5 print(error)
6 finally:
7 print("This is executed last!")

NameError: name 'error' is not defined

Why the exceptions messages are printed at the end of the output, not between “1” and “This is executed last”?

While catching the error for print(10 / 0) the system found another exception in the except block, the undeclared variable error raising NameError exception. So nothing was printed. This inner NameError exception was uncaught by program and can only printed after finally block.


An assertion is a sanity-check where an expression is tested, and if the result comes up false, an exception is raised. When it encounters an assert statement, Python evaluates the accompanying expression, which is expected to be true. If the expression is false, Python raises an AssertionError exception.

AssertionError exceptions can be caught and handled like any other exception using the try-except statement, but if not handled, this type of exception will terminate the program.

But what makes assertion different from try/except?

An assertion would stop the program from running (because you should fix that error or the program is useless), but an exception would let the program continue running (if you use else or finally). In other words, exceptions address the robustness of your application while assertions address its correctness.

Assertions should be used to check something that should never happen while an exception should be used to check something that might happen(something in which you don’t have control like user input).

NOTE: The rule is that use assertions when you are trying to catch your own errors (functions or data that are internal to your system), and exceptions when trying to catch other people’s errors.

>>> def KelvinToFahrenheit(temp):
assert (temp >= 0),"Colder than absolute zero? Go back to school. -_-"
return ((temp - 273)*1.8) + 32

>>> print(KelvinToFahrenheit(273))
>>> print(KelvinToFahrenheit(-5))
>>> print(KelvinToFahrenheit(373))

AssertionError Traceback (most recent call last)
<ipython-input-8-832b1596a334> in <module>
5 print(KelvinToFahrenheit(273))
----> 6 print(KelvinToFahrenheit(-5))
7 print(KelvinToFahrenheit(373))

<ipython-input-8-832b1596a334> in KelvinToFahrenheit(temp)
1 def KelvinToFahrenheit(temp):
----> 2 assert (temp >= 0),"Colder than absolute zero? Go back to school. -_-"
3 return ((temp - 273)*1.8) + 32
5 print(KelvinToFahrenheit(273))

AssertionError: Colder than absolute zero? Go back to school. -_-

As mentioned in the previous article, I will be attaching the notebook at the end. So yeah, we have reached the end of this article.

I hope you guys have enjoyed this article. For any questions or doubts, please comment below and share your feedback on the notebook. Stay tuned for the part 2. Till then, keep coding!