Python Advanced

Disclaimer - This document is only meant to serve as a reference for the attendees of the workshop. It does not cover all the concepts or implementation details discussed during the actual workshop.

Variable Scope

When you program, you'll often find that similar ideas come up again and again. You'll use variables for things like counting, iterating and accumulating values to return. In order to write readable code, you'll find yourself wanting to use similar names for similar ideas. As soon as you put multiple piece of code together (for instance, multiple functions or function calls in a single script) you might find that you want to use the same name for two separate concepts.

Fortunately, you don't need to come up with new names endlessly. Reusing names for objects is OK as long as you keep them in separate scope. "Scope" refers to which parts of a program a variable can be referenced from.

If a variable is created inside a function, it can only be used within that function.

Consider these two functions, word_count and nearest_square. Both functions include a answer variable, but they are distinct variables that only exist within their respective functions:

In [1]:
def word_count(document, search_term):
    """ Count how many times search_term appears in document. """
    words = document.split()    
    answer = 0
    for word in words:
        if word == search_term:
            answer += 1
    return answer

def nearest_square(limit):
    """ Find the largest square number smaller than limit. """
    answer = 0
    while (answer+1)**2 < limit:
        answer += 1
    return answer**2

Since the variable answer here is defined within each function seperately, you can reuse the same name of the variable, as the scope of the variables itself is different. Note : Functions, however, can access variables that are defined outside of its scope or in the larger scope, but can only read the value of the variable, not modify it. This is shown by the UnboundLocalError in the example below.

In [2]:
egg_count = 0

def buy_eggs():
    egg_count += 12 # purchase a dozen eggs

# buy_eggs()

In such situations its better to redefine the functions as below.

In [3]:
egg_count = 0

def buy_eggs():
    return egg_count + 12

egg_count = buy_eggs()
egg_count = buy_eggs()


List Basics

In Python, it is possible to create a list of values. Each item in the list is called an element and can be accessed individually using a zero-based index. Hence avoiding the need to create multiple variables to store individual values.
Note: negative indexes help access elements from the end of the array. -1 refers to the last element and -2 refers to the second last element and so on.

In [4]:
# list of numbers of type Integer
numbers = [1, 2, 3, 4, 5]
print("List :", numbers)
print("Second element :", numbers[1])    ## 2
print("Length of list :",len(numbers))   ## 5
print() # Empty line

# list of strings
colors = ['red', 'blue', 'green']
print("List :", colors)
print ("First color :", colors[0])        ## red
print ("Third color :", colors[2])        ## green
print ("Last color :", colors[-1])        ## green
print ("Second last color :", colors[-2]) ## blue
print ("Length of list :",len(colors))    ## 3
print() # Empty line

# list with multiple variable types
me = ['Shantanu Kamath', 'Computer Science', 20, 1000000]
print("List :", me)
print("Fourth element :", me[3])        ## 1000000
print("Length of list :", len(me))      ## 4

List : [1, 2, 3, 4, 5]
Second element : 2
Length of list : 5

List : ['red', 'blue', 'green']
First color : red
Third color : green
Last color : green
Second last color : blue
Length of list : 3

List : ['Shantanu Kamath', 'Computer Science', 20, 1000000]
Fourth element : 1000000
Length of list : 4

Since lists are considered to be sequentially ordered, they support a number of operations that can be applied to any Python sequence.

Operation Name Operator Explanation
Indexing [ ] Access an element of a sequence
Concatenation + Combine sequences together
Repetition * Concatenate a repeated number of times
Membership in Ask whether an item is in a sequence
Length len Ask the number of items in the sequence
Slicing [ : ] Extract a part of a sequence

In [5]:
myList = [1,2,3,4]

# Indexing
A = myList[2]

# Repititoin
A = [A]*3

# Concatenation
print(myList + A)

# Membership
print(1 in myList)

# Length

# Slicing [inclusive : exclusive]

# Leaving the exclusive parameter empty

[3, 3, 3]
[1, 2, 3, 4, 3, 3, 3]
[2, 3]
[2, 3, 4]


Strings are immutable and list are mutable.
For example :

In [6]:
# Creating sentence and list form of sentence
name = "Welcome to coding with Python v3.6"
words = ["Welcome", "to", "coding", "with", "Python", "v3.6"]


# This is okay
words[5] = "v2.7"

# This is not
# name[5] = "d"
# print(name)

['Welcome', 'to', 'coding', 'with', 'Python', 'v2.7']

Passed by reference

The list is stored at a memory locations and only a reference of this memory location is what the variable holds. So changes applied to one variable reflect in other variables as well.

In [7]:
langs = ["Python", "Java", "C++", "C"]
languages = langs

['Python', 'Java', 'C++', 'C', 'C#']
['Python', 'Java', 'C++', 'C', 'C#']

List Methods

Besides simple accessing of values, lists have a large variety of methods that are used to performed different useful manipulations on them.
Some of them are:

  • list.append(element): adds a single element to the end of the list. Common error: does not return the new list, just modifies the original.

In [8]:
# list.append example
names = ['Hermione Granger', 'Ronald Weasley']
names.append('Harry Potter')
print("New list :", names)  ## ['Hermione Granger', 'Ronald Weasley', 'Harry Potter']

New list : ['Hermione Granger', 'Ronald Weasley', 'Harry Potter']
  • list.insert(index, element): inserts the element at the given index, shifting elements to the right.

In [9]:
# list.insert example
names = ['Ronald Weasley', 'Hermione Granger']
names.insert(1, 'Harry Potter')
print("New list :", names)  ## ['Ronald Weasley', 'Harry Potter', 'Hermione Granger']

New list : ['Ronald Weasley', 'Harry Potter', 'Hermione Granger']
  • list.extend(list2): adds the elements in list2 to the end of the list. Using + or += on a list is similar to using extend().

In [10]:
# list.extend example
MainChar = ['Ronald Weasley', 'Harry Potter', 'Hermione Granger']
SupChar = ['Neville Longbottom', 'Luna Lovegood']
print("Full list :", MainChar)  ## ['Ronald Weasley', 'Harry Potter', 'Hermione Granger', 'Neville Longbottom', 'Luna Lovegood']

Full list : ['Ronald Weasley', 'Harry Potter', 'Hermione Granger', 'Neville Longbottom', 'Luna Lovegood']
  • list.index(element): searches for the given element from the start of the list and returns its index. Throws a ValueError if the element does not appear (use 'in' to check without a ValueError).

In [11]:
# list.index example
names = ['Ronald Weasley', 'Harry Potter', 'Hermione Granger']
index = names.index('Harry Potter')  
print("Index of Harry Potter in list :",index)  ## 1

# Throws a ValueError (Uncomment to see error.)
# index = names.index('Albus Dumbledore')

Index of Harry Potter in list : 1
  • list.remove(element): searches for the first instance of the given element and removes it (throws ValueError if not present)

In [12]:
names = ['Ronald Weasley', 'Harry Potter', 'Hermione Granger']
index = names.remove('Harry Potter')  ## ['Ronald Weasley', 'Hermione Granger']
print("Modified list :", names)

Modified list : ['Ronald Weasley', 'Hermione Granger']
  • list.pop(index): removes and returns the element at the given index. Returns the rightmost element if index is omitted (roughly the opposite of append()).

In [13]:
names = ['Ronald Weasley', 'Harry Potter', 'Hermione Granger']
index = names.pop(1)
print("Modified list :", names)  ## ['Ronald Weasley', 'Hermione Granger']

Modified list : ['Ronald Weasley', 'Hermione Granger']
  • list.sort(): sorts the list in place (does not return it). (The sorted() function shown below is preferred.)

In [14]:
alphabets = ['a', 'f','c', 'e','b', 'd']
print ("Sorted list :", alphabets) ## ['a', 'b', 'c', 'd', 'e', 'f']

Sorted list : ['a', 'b', 'c', 'd', 'e', 'f']
  • list.reverse(): reverses the list in place (does not return it).

In [15]:
alphabets = ['a', 'b', 'c', 'd', 'e', 'f']
print("Reversed list :", alphabets)  ## ['f', 'e', 'd', 'c', 'b', 'a']

Reversed list : ['f', 'e', 'd', 'c', 'b', 'a']

Others methods include :

  • Count : list.count()
  • Delete : del list[index]
  • Join : "[Seperator string]".join(list)

List Comprehensions

In Python, List comprehensions provide a concise way to create lists. Common applications are to make new lists where each element is the result of some operations applied to each member of another sequence, or to create a subsequence of those elements that satisfy a certain condition.

It can be used to construct lists in a very natural, easy way, like a mathematician is used to do. This is how we can explain sets in maths:

  • Squares = {x² : x in {0 ... 9}}
  • Exponents = (1, 2, 4, 8, ..., 2¹²)
  • EvenSquares = {x | x in S and x even}

Lets try to do this in Python using normal loops and list methods:

In [16]:
# Using loops and list methods

squares = []
for x in range(10):
print("Squares :", squares)           ## [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
exponents = []
for i in range(13):
print("Exponents :", exponents)       ## [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]

evenSquares = []
for x in squares:
  if x % 2 == 0:
print("Even Squares :", evenSquares)  ## [0, 4, 16, 36, 64]

Squares : [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Exponents : [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
Even Squares : [0, 4, 16, 36, 64]

These extend to more than one line. But by using list comprehensions you can bring it down to just one line.

In [17]:
# Using list comprehensions

squares = [x**2 for x in range(10)]
exponents = [2**i for i in range(13)]
evenSquares = [x for x in squares if x % 2 == 0]
print("Squares :", squares)           ## [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print("Exponents :", exponents)       ## [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
print("Even Squares :", evenSquares)  ## [0, 4, 16, 36, 64]

Squares : [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Exponents : [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
Even Squares : [0, 4, 16, 36, 64]


Searching is the process of finding a particular item in a collections of items. It is one of the most common problems that arise in computer programming. A search typically answers either True or False as to whether the item is present.

In Python, there is a very easy way to ask whether an item is in a list of items. We use the in operator.

In [18]:
# Using in to check if number is present in the list.

print(15 in [3,5,2,4,1])
print('Work' in 'Python Advanced Workshop')


Sometimes it can be important to get the position of the searched value. In that case, we can use index method for lists and the find method for strings.

In [19]:
# Using index to get position of the number if present in list.
# In case of lists, its important to remember that the index function will throw an error if the value isn't present in the list.

values = [3,5,2,4,1]
if 5 in values:
  print("Value present at",values.index(5))  ## 1
  print("Value not present in list")

# Using find to get the index of the first occurrence of the word in a sentence.

sentence = "This be a string"
index = sentence.find("is")
if index == -1:
  print("There is no 'is' here!")
  print("Found 'is' in the sentence at position "+str(index))

# Using index to find words in a list of words
sentence = "This be a string"
words = sentence.split(' ')
if 'is' in words:
  print("Found 'is' in the list at position "+str(words.index('is')))
  print("There is no 'is' here!")

Value present at 1
Found 'is' in the sentence at position 2
There is no 'is' here!

For more efficient Search Algorithms, look through the Algorithm Implementation section of this repository


Sorting is the process of placing elements from a collection in some kind of order.
For example, a list of words could be sorted alphabetically or by length.
A list of cities could be sorted by population, by area, or by zip code.

Python lists have a built-in sort() method that modifies the list in-place and a sorted() built-in function that builds a new sorted list from an iterable.

  • list.sort(): Modifies existing list and can be used only with lists.
  • sorted(list): Creates a new list when called and can be used with other iterables.
Basic sorting functions

The most basic use of the sorted function can be seen below :

In [20]:
# Using sort() with a list.

values = [7, 4, 3, 6, 1, 2, 5]
print("Unsorted list :", values)     ## [7, 4, 3, 6, 1, 2, 5]
newValues = values.sort()
print("New list :", newValues)       ## None
print("Old list :", values)          ## [1, 2, 3, 4, 5, 6, 7]
# Using sorted() with a list.

values = [7, 4, 3, 6, 1, 2, 5]
print("Unsorted list :", values)     ## [7, 4, 3, 6, 1, 2, 5]
newValues = sorted(values)
print("New list :", newValues)       ## [1, 2, 3, 4, 5, 6, 7]
print("Old list :", values)          ## [7, 4, 3, 6, 1, 2, 5]

Unsorted list : [7, 4, 3, 6, 1, 2, 5]
New list : None
Old list : [1, 2, 3, 4, 5, 6, 7]

Unsorted list : [7, 4, 3, 6, 1, 2, 5]
New list : [1, 2, 3, 4, 5, 6, 7]
Old list : [7, 4, 3, 6, 1, 2, 5]
Sorting using additional key

For more complex custom sorting, sorted() takes an optional "key=" specifying a "key" function that transforms each element before comparison.
The key function takes in 1 value and returns 1 value, and the returned "proxy" value is used for the comparisons within the sort.

In [21]:
# Using key in sorted

values = ['ccc', 'aaaa', 'd', 'bb']
print (sorted(values, key=len))            ## ['d', 'bb', 'ccc', 'aaaa']

# Remember case sensitivity :  All upper case characters come before lower case character in an ascending sequence.
sentence = "This is a test string from Andrew"
print(sorted(sentence.split(), key=str.lower))    ## ['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']

# Using reverse for ascending and descending
strs = ['aa', 'BB', 'zz', 'CC']
print (sorted(strs))                         ## ['BB', 'CC', 'aa', 'zz'] (case sensitive)
print (sorted(strs, reverse=True))          ## ['zz', 'aa', 'CC', 'BB']

['d', 'bb', 'ccc', 'aaaa']
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
['BB', 'CC', 'aa', 'zz']
['zz', 'aa', 'CC', 'BB']

Basics on Class and OOP

This section is built around the fundamental of Object Oriented Programming (OOP). It aims strengthening basics but doesn't justify the broad topic itself. As OOP is a very important programming concept you should read further to better get a grip on python as well as go deep in understanding how it is useful and essential to programming. Below are some essential resources :


In all the code we wrote till now, we have designed our program around functions i.e. blocks of statements which manipulate data. This is called the procedure-oriented way of programming.

There is another way of organizing your program which is to combine data and functionality and wrap it inside something called an object. This is called the object oriented programming paradigm.

Most of the time you can use procedural programming, but when writing large programs or have a problem that is better suited to this method, you can use object oriented programming techniques.

Classes and Objects

Classes and objects are the two main aspects of object oriented programming. A class creates a new type where objects are instances of the class.

Objects can store data using ordinary variables that belong to the object. Variables that belong to an object or class are referred to as fields or attributes.

Objects can also have functionality by using functions that belong to a class. Such functions are called methods of the class.

The simplest class possible is shown in the following example :

In [22]:
class Person:
    pass  # An empty block

p = Person()

<__main__.Person object at 0x1049b64a8>


Class methods have only one specific difference from ordinary functions - they must have an extra first name that has to be added to the beginning of the parameter list, but you do not give a value for this parameter when you call the method, Python will provide it. This particular variable refers to the object itself, and by convention, it is given the name self.

In [23]:
class Person:
    def say_hi(self):
        print('Hello, how are you?')

p = Person()

Hello, how are you?

The init

There are many method names which have special significance in Python classes. We will see the significance of the init method now.

The init method is run as soon as an object of a class is instantiated. The method is useful to do any initialization you want to do with your object. Notice the double underscores both at the beginning and at the end of the name.

In [24]:
class Person:
    def __init__(self, name): = name

    def say_hi(self):
        print('Hello, my name is',

p = Person('Shantanu')

Hello, my name is Shantanu

Object variables

Now let us learn about the data part. The data part, i.e. fields, are nothing but ordinary variables that are bound to the namespaces of the classes and objects. This means that these names are valid within the context of these classes and objects only. That's why they are called name spaces.

There are two types of fields - class variables and object variables which are classified depending on whether the class or the object owns the variables respectively.

Class variables are shared - they can be accessed by all instances of that class. There is only one copy of the class variable and when any one object makes a change to a class variable, that change will be seen by all the other instances.

Object variables are owned by each individual object/instance of the class. In this case, each object has its own copy of the field i.e. they are not shared and are not related in any way to the field by the same name in a different instance.

An example will make this easy to understand.

In [25]:
class Robot:
    ##   Represents a robot, with a name.

    # A class variable, counting the number of robots
    population = 0

    def __init__(self, name):
        ##   Initializes the data. = name
        print("(Initializing {})".format(

        # When this person is created, the robot
        # adds to the population
        Robot.population += 1

    def die(self):
        ##   I am dying.
        print("{} is being destroyed!".format(

        Robot.population -= 1

        if Robot.population == 0:
            print("{} was the last one.".format(
            print("There are still {:d} robots working.".format(

    def say_hi(self):
        ##   Greeting by the robot. Yeah, they can do that.
        print("Greetings, my masters call me {}.".format(

    def how_many(cls):
        ##   Prints the current population.
        print("We have {:d} robots.".format(cls.population))

droid1 = Robot("R2-D2")

droid2 = Robot("C-3PO")

print("\nRobots can do some work here.\n")

print("Robots have finished their work. So let's destroy them.")


(Initializing R2-D2)
Greetings, my masters call me R2-D2.
We have 1 robots.
(Initializing C-3PO)
Greetings, my masters call me C-3PO.
We have 2 robots.

Robots can do some work here.

Robots have finished their work. So let's destroy them.
R2-D2 is being destroyed!
There are still 1 robots working.
C-3PO is being destroyed!
C-3PO was the last one.
We have 0 robots.

How It Works

This is a long example but helps demonstrate the nature of class and object variables. Here, population belongs to the Robot class and hence is a class variable. The name variable belongs to the object (it is assigned using self) and hence is an object variable.

Thus, we refer to the population class variable as Robot.population and not as self.population. We refer to the object variable name using notation in the methods of that object. Remember this simple difference between class and object variables. Also note that an object variable with the same name as a class variable will hide the class variable!

Instead of Robot.population, we could have also used self.class.population because every object refers to its class via the self.class attribute.

The how_many is actually a method that belongs to the class and not to the object. This means we can define it as either a classmethod or a staticmethod depending on whether we need to know which class we are part of. Since we refer to a class variable, let's use classmethod.

We have marked the how_many method as a class method using a decorator. Decorators can be imagined to be a shortcut to calling a wrapper function, so applying the @classmethod decorator is same as calling: how_many = classmethod(how_many)

Observe that the init method is used to initialize the Robot instance with a name. In this method, we increase the population count by 1 since we have one more robot being added. Also observe that the values of is specific to each object which indicates the nature of object variables. Remember, that you must refer to the variables and methods of the same object using the self only. This is called an attribute reference.

All class members are public. One exception: If you use data members with names using the double underscore prefix such as __privatevar , Python uses name-mangling to effectively make it a private variable.

Thus, the convention followed is that any variable that is to be used only within the class or object should begin with an underscore and all other names are public and can be used by other classes/objects. Remember that this is only a convention and is not enforced by Python (except for the double underscore prefix).

There are more concepts in OOP such as Inheritance, Abstraction and Polymorphism, which would require a lot more time to cover. You may refer to reference material for explanation on these topics.

File I/O

File handling is super simplified in Python compared to other programming languages. The first thing you’ll need to know is Python’s built-in open function to get a file object.
The open function opens a file. When you use the open function, it returns something called a file object. File objects contain methods and attributes that can be used to collect information about the file you opened. They can also be used to manipulate said file.

For example, the mode attribute of a file object tells you which mode a file was opened in. And the name attribute tells you the name of the file that the file object has opened.

File Types

In Python, a file is categorized as either text or binary, and the difference between the two file types is important.

Text files are structured as a sequence of lines, where each line includes a sequence of characters. This is what you know as code or syntax.

Each line is terminated with a special character, called the EOL or End of Line character. There are several types, but the most common is the comma {,} or newline character. It ends the current line and tells the interpreter a new one has begun.

A backslash character can also be used, and it tells the interpreter that the next character – following the slash – should be treated as a new line. This character is useful when you don’t want to start a new line in the text itself but in the code.

A binary file is any type of file that is not a text file. Because of their nature, binary files can only be processed by an application that know or understand the file’s structure. In other words, they must be applications that can read and interpret binary.

Open ( ) Function

The syntax to open a file object in Python is:

file_object  = open("filename", "mode") ## where file_object is the variable to add the file object.

The second argument you see – mode – tells the interpreter and developer which way the file will be used.


Including a mode argument is optional because a default value of r will be assumed if it is omitted.

The modes are:

  • r – Read mode which is used when the file is only being read
  • w – Write mode which is used to edit and write new information to the file (any existing files with the same name will be erased when this mode is activated)
  • a – Appending mode, which is used to add new data to the end of the file; that is new information is automatically amended to the end
  • r+ – Special read and write mode, which is used to handle both actions when working with a file

Create a text file

Using a simple text editor, let’s create a file. You can name it anything you like, and it’s better to use something you’ll identify with. For the purpose of this workshop, however, we are going to call it "testfile.txt". Just create the file and leave it blank.

To manipulate the file :

file = open("testfile.txt","w")

file.write("Hello World")
file.write("This is our new text file")
file.write("and this is another line.")
file.write("Why? Because we can.")


Reading a text file

Following methods allow reading a file :

  • extract a string that contains all characters in the file.
    file = open("testfile.text", "r")
  • extract only a certain number of characters.
    file = open("testfile.txt", "r")
  • file.readline(): read a file line by line – as opposed to pulling the content of the entire file at once.
    file = open("testfile.txt", "r")
  • file.readline(lineNumber): return a specific line
    file = open("testfile.txt", "r")
  • file.readlines(): return every line in the file, properly separated in a list
    file = open("testfile.txt", "r")
    print (file.readlines())

Looping over file

When you want to read – or return – all the lines from a file in a more memory efficient, and fast manner, you can use the loop over method. The advantage to using this method is that the related code is both simple and easy to read.

file = open("testfile.txt", "r")
for line in file:
print line

Using the File Write Method

This method is used to add information or content to an existing file. To start a new line after you write data to the file, you can add an EOL ("\n")) character.

file = open("testfile.txt", "w")

file.write("This is a test")
file.write("To add more lines.")


Closing a file

When you’re done working, you can use the fh.close() command to end things. What this does is close the file completely, terminating resources in use, in turn freeing them up for the system to deploy elsewhere.

It’s important to understand that when you use the fh.close() method, any further attempts to use the file object will fail.


Exception Handling

There are two types of errors that typically occur when writing programs. The first, known as a syntax error, simply means that the programmer has made a mistake in the structure of a statement or expression. For example, it is incorrect to write a for statement and forget the colon.

In [26]:
# ( Uncomment to see Syntax error. )
# for i in range(10)

The other type of error, known as a logic error, denotes a situation where the program executes but gives the wrong result. This can be due to an error in the underlying algorithm or an error in your translation of that algorithm. In some cases, logic errors lead to very bad situations such as trying to dividing by zero or trying to access an item in a list where the index of the item is outside the bounds of the list. In this case, the logic error leads to a runtime error that causes the program to terminate. These types of runtime errors are typically called exceptions.

When an exception occurs, we say that it has been raised. You can handle the exception that has been raised by using a try statement. For example, consider the following session that asks the user for an integer and then calls the square root function from the math library. If the user enters a value that is greater than or equal to 0, the print will show the square root. However, if the user enters a negative value, the square root function will report a ValueError exception.

In [27]:
import math
anumber = int(input("Please enter an integer "))
# Give input as negative number and also see output from next code snippet

Please enter an integer -10
ValueError                                Traceback (most recent call last)
<ipython-input-27-ef0cdd33095c> in <module>()
      2 anumber = int(input("Please enter an integer "))
      3 # Give input as negative number and also see output from next code snippet
----> 4 print(math.sqrt(anumber))

ValueError: math domain error

We can handle this exception by calling the print function from within a try block. A corresponding except block catches the exception and prints a message back to the user in the event that an exception occurs. For example:

In [28]:
   print("Bad Value for square root")
   print("Using absolute value instead")

Bad Value for square root
Using absolute value instead

It is also possible for a programmer to cause a runtime exception by using the raise statement. For example, instead of calling the square root function with a negative number, we could have checked the value first and then raised our own exception. The code fragment below shows the result of creating a new RuntimeError exception. Note that the program would still terminate but now the exception that caused the termination is something explicitly created by the programmer.

In [29]:
if anumber < 0:
    raise RuntimeError("You can't use a negative number")

RuntimeError                              Traceback (most recent call last)
<ipython-input-29-c2dee6db69a9> in <module>()
      1 if anumber < 0:
----> 2     raise RuntimeError("You can't use a negative number")
      3 else:
      4     print(math.sqrt(anumber))

RuntimeError: You can't use a negative number

There are many kinds of exceptions that can be raised in addition to the RuntimeError shown above. See the Python reference manual for a list of all the available exception types and for how to create your own.

Importing Modules

So far we haven't explained what a Python module is. To put it in a nutshell: every file, which has the file extension .py and consists of proper Python code, can be seen or is a module.
There is no special syntax required to make such a file a module. A module can contain arbitrary objects, for example files, classes or attributes.
All those objects can be accessed after an import. There are different ways to import a modules.

Import one module:

>>> import math
>>> math.pi
>>> math.sin(math.pi/2)
>>> math.cos(math.pi/2)
>>> math.cos(math.pi)

Import more than one module in one import statement:

import math, random

If only certain objects of a module are needed, we can import only those:

from math import sin, pi

Instead of explicitly importing certain objects from a module, it's also possible to import everything in the namespace of the importing module:

>>> from math import *
>>> sin(3.01) + tan(cos(2.1)) + e
>>> e


This topic is a little more complex than you would have expected it to be. Probably practice more python before you venture into this topic. But just to get you to understand what this topic consists of and also appreciate the powerful nature of python, Here are a few video clips to showing the result of various python scripts.
Articles showcasing python scripts
Movie subtitle downloader
Python Script to download course content from Blackboard learn