
In Python, we structure programs as a sequence of statements.
When you execute a Python program, the interpreter executes each statement until there are no statements to execute or it encounter an error while executing an instruction. We can test this out by writing a simple python script.
print("This statement will be executed")
(1,2).append(1)
print("This statement will not be executed")
We can execute the script using the command python3 file1.py.
Python encounters the following error while executing the script.
In the above code listing, Python executes the first print
statement and encounters an error while executing the second.
In the second statement, we try to append a number to a tuple()
. Tuples don't have a method to append an element, and therefore Python raises AttributeError
.
As I mentioned before, Python executes a program or script as a series of statements. If it encounters an error at a statement, the next statements are not executed by Python.
When executing the script file1.py
, we will get the following output.
This statement will be executed
Traceback (most recent call last):
File "file1.py", line 2, in <module>
(1,2).append(1)
AttributeError: 'tuple' object has no attribute 'append'
In the above code listing, Python executes the first print()
function and encounters an error while executing the second. Therefore, it skips the third one.
So far, we saw that each statement occupied a single line. We can write multiple statements in a single line by separating them with a ;
semicolon. For instance, a = 10; b = 10
A statement can be written in a separate line or written over multiple lines in a Python program. For example, in the following script, we create a list containing days spanning multiple lines.
days_names = [ "Monday", "Tuesday",
"Wednesday", "Thursday",
"Friday", "Saturday",
"Sunday"]
Even though the script's statements above span multiple lines, Python considers the assignment operation a single line or specifically a single logical line. The multiple lines you and I see in the above code are called physical lines.
How many logical lines and physical lines are present in the code below?
>>> person = {
"name": "Luffy", "friends": ["Zorro", "Sanji"],
"position": "Captain"
}
- Logical Lines: 4, Physical Lines: 4
- Logical Lines: 4, Physical Lines: 1
- Logical Lines: 1, Physical Lines: 4
- Logical Lines: 1, Physical Lines: 1
Let's understand more about how Python interprets the program using logical lines in the next section.
Logical Lines
Python interpreter breaks down the script or program into a stream of tokens, and this process is called tokenizing. Using the tokens generated, Python breaks down the program in different logical lines. A logical line is what Python sees as a single statement. For instance,
i = 10
print(i)
In the above code, we can see two statements spread over two lines. Each statement above is a logical line. However, we can also write the two statements above in a single line.
i = 10; print(i);
In this case, there are two statements in a single line, but Python sees two logical lines. The line which you see when you write the program is called a physical line.

A physical line is a sequence of characters terminated by an End-of-Line (EOL) sequence. End-of-Line (EOL) sequence moves the cursor both down to the next line and to the beginning of that line. Earlier, when we wanted to print strings spanning multiple lines, we used the sequence \n
to do so.
>>> print("This spans \nthree lines. \nHurray ")
This spans
three lines.
Hurray
As we mentioned earlier, the Python interpreter converts the script into stream of tokens. In the generated tokens, Python denotes the end of a logical using token NEWLINE
.
Let's write a program to understand this more.
animal = [ "cat",
"dog"]
print("My favorite animal is a {}".format(animal[1]))
The output of the script file2.py
is shown below.
My favorite animal is a dog.
How many logical and physical lines are in the script file2.py
?
animal = [ "cat",
"dog"]
print("My pet animal is a {}".format(animal[1]))
- Logical lines: 1, Physical lines: 3
- Logical lines: 2, Physical lines: 3
- Logical lines: 3, Physical lines: 3
- Logical lines: 1, Physical lines: 1
We can also confirm that the above exercise is correct by verifying the tokens generated. Python provides a way to check the tokens generated by a source program using the tokenize
module.
You can directly run the tokenize module on a python script from the command prompt using the -m
flag, as shown below.
python -m tokenize [-e] [filename.py]
It is tokenizing the source program of file2.py
results in the following output.
0,0-0,0: ENCODING 'utf-8'
1,0-1,6: NAME 'animal'
1,7-1,8: EQUAL '='
1,9-1,10: LSQB '['
1,11-1,16: STRING '"cat"'
1,16-1,17: COMMA ','
1,17-1,18: NL '\n'
2,3-2,8: STRING '"dog"'
2,8-2,9: RSQB ']'
2,10-2,11: NEWLINE '\n'
3,0-3,5: NAME 'print'
3,5-3,6: LPAR '('
3,6-3,32: STRING '"My favorite animal is {}"'
3,32-3,33: DOT '.'
3,33-3,39: NAME 'format'
3,39-3,40: LPAR '('
3,40-3,46: NAME 'animal'
3,46-3,47: LSQB '['
3,47-3,48: NUMBER '1'
3,48-3,49: RSQB ']'
3,49-3,50: RPAR ')'
3,50-3,51: RPAR ')'
3,51-3,52: NEWLINE '\n'
4,0-4,0: ENDMARKER ''
In the output, the first column represents the line and column number of the token. The second column provides the token name. At the same time, the last shows the value of the token.
In the tokens, we can see that there are only two NEWLINE tokens even though there are three lines in the source code. The token NL is used to indicate a non-terminating newline.
Python generates NL tokens when a logical line of code spans over multiple physical lines.
Python considers the assignment statement to the name animal
that spans two physical lines as a single logical line. The NL
token type represents newline characters (\n
or \r\n
) that do not end logical code lines. Newlines that do end logical lines of Python code are indicated using the NEWLINE
token.
Python encourages the use of a single statement per line, which makes the code more readable. Therefore, write a single logical line in a single physical line.
If you want to specify more than one logical line on a single physical line, you have to specify this using a semicolon explicitly (;
) , indicating the end of a logical line/statement.
Note: The token generated by ;
semicolon is OP.
Earlier, we wrote two logical lines on a single physical line using a semicolon (;
), indicating the end of a logical line/statement. Python implicitly assumes that each physical line is a logical line unless we use a semicolon (;
).
Use more than a single physical line only if the logical line is long.
Avoid semicolon (;
)
Python generates all the logical lines from one or more physical lines.
a = [1,
2]
b = [3, 4]
c = b + c
How many NL
tokens are generated from the script below?
a = [1,
2]
b = [3, 4]
c = b + c
- 1
- 2
- 3
- 4
Often two or more physical lines are joined together to form a logical. Let's see understand how line-joining works in Python.
Explicit Line Joining
We can join two or more physical lines into a logical line using backslash characters (\
), also called the line continuation character. For instance, we can write the statement 45 + 5 + 50 over multiple lines in the following way.
>>> 45 \
... + 5 \
... + 50
100
When a physical line ends with a backslash, which is not a part of a string literal or comment, Python joins the line to form a single logical statement. Joining two or more physical lines using the line continuation character is called explicit line joining. We are explicitly telling Python to join these lines together. In explicit line joining, you cannot add comments after the backslash character (\
) .
For instance, this code expression is valid.
>>> a = 45 \
... + 5
>>> a
50
While the following code is invalid,
>>> a = 45 \ # naming 45 + 5 as `a`
... + 5
File "<stdin>", line 1
a = 45 \ # naming 45 + 5 as `a`
^
SyntaxError: unexpected character after line continuation character
Adding anything after the backslash character is prohibited in Python.
Will this piece of code correctly execute in Python?
>>> list_of_countries = ["India", "Bhutan" ] + \ ["Sri Lanka"] \
... + ["Japan", "Russia"]
- Yes
- No
Python raises SyntaxError
when if you put character(s) after the line continuation character. The other form of lines joining in Python is done implicitly, meaning in a way that is not directly expressed. Let's have a look.
Implicit Line Joining
Python allows the splitting of certain expressions over multiple physical lines. These expressions don't require a backslash character (\
) at the end of their line to form a single logical line. The following are such expressions:
- expressions within a parenthesis
()
, brackets[]
or braces{}
- function arguments
- triple-quoted string literal
For instance.
>>> (45 + # Parenthesis
... 5
... + 50)
100
>>> [40, # Brackets
... 50,
... 60,
... 70]
[40, 50, 60, 70]
>>> { # Braces
... "name" : "John Doe",
... "age": 26
... }
{'name': 'John Doe', 'age': 26}
>>> """This string
... spans
... multiple
... lines"""
'This string\nspans \nmultiple \nlines'# Triple-quoted literals
In any of the above formats, expressions can span over multiple physical lines. Python joins multiples physical lines to a single logical line. The automatic joining of numerous physical lines by Python is called implicit line joining.
Essential characteristics of the implicitly joined lines are as follows:
- can carry comments, except for triple-quoted strings
- indentation of continuation lines is not important
- blank lines are allowed
The ability to add comments is really useful sometimes to increase code-readability.
>>> guests = [
"Tyrion", # Imp
"Harry", # Wizard
"Luffy", # Pirate
]
>>> guests
['Tyrion', 'Harry', 'Luffy']
Adding comments to function arguments is useful to understand the high-level overview of the function quickly.
>>> def my_func(arg1, # A Comment
... arg2, # Another Comment
... arg3 # Yet another comment
... ):
... pass
Adding comments to function arguments is useful to understand the high-level overview of the function quickly.
Earlier, we mentioned the Python executes every statement in order until
- either there are no more statements to execute
- or Python encounters an error while executing a statement
Apart from that, there are particular statement(s) or code block whose execution is dependent on some conditions. When Python executes a code depending on some condition, it is called conditional execution.
We can control the usual order of execution of statements by using some particular Python keywords. In Python, statements using the keywords if-else
, while
, and for
implement control flow constructs. Let's learn more control-flow statements in the next section.
Control Flow
In programming, control flow refers to the order of execution of statements in a program. Most programming languages allow you to control the flow of executions of statements.
Python executes one statement after another from top to bottom without skipping any statements. Two important methods of controlling the flow of execution are conditionals execution or loops. Let's take a look at conditional execution.
Conditional Execution
When we instruct Python to execute a block or piece of code only when it satisfies a condition, it is called conditional execution.
Conditional execution is implemented in Python using if-else
statements. In-fact, most programming languages contain if-else
statements. The keywords, if
, else
, and elif
control code execution flow.
The general syntax of if-else
statements or simply if
statements is the following:
if condition1:
# Execute this code block
elif condition2:
# Execute this code block
elif condition3:
# Execute this code block
else:
# If none of the above conditions are true
# Execute this code block
In the above code, Python evaluates the conditions one by one until it finds a True
condition. Python executes the code block corresponding to that condition. If none of the conditions are True
, Python executes the code block inside the else
statement.

Figure 2 shows a diagram representing the flow of execution of statements in Python.
The elif
and else
statements are optional.
Both if
and elif
keyword requires conditions to check if they can execute their code block. The conditions are usually in the form of Boolean expressions, which means the expressions upon evaluation will either return True
or False
.
For instance,
>>> a = 5
>>> if a < 6:
... print("a is less than 6")
...
a is less than 6
In the above code listing, the expression a < 6 is the Boolean expression provided as a condition for the if
statement.
What do you think is the output when the following script is run?
text = "The quick brown fox."
if 'a' in text:
(1, 2).append(3)
print("The letter a is present in text")
elif 'b' in text:
print("The letter b is present in text")
elif 'c' in text:
(1, 2).append(3)
print("The letter c is present in text")
else:
(1, 2).append(3)
print("No result Found")
- The letter
a
is present in the text - The letter
b
is present in the text - No result Found
- Raises
AttributeError
The code in the previous exercise outputs The letter a is present in text.
Appending an integer to a tuple should have caused Python to raise AttributeError
. However, Python evaluates an if-elif
code block only when the condition evaluates to True
. Python executes the code block in the else
statement if the conditions provided to all the other if-else
statements are False
.
Therefore, Python doesn't raise any errors while executing the script above.
We mentioned earlier that we could test any object in Python for its truth value. You can put any object as a condition for if-else
statements. When you place an object as a condition for if-else
statements, Python evaluates it to be the same as using the built-in bool(<obj>)
function on the object.
>>> b = [10, 20]
>>> if b: # Condition evaluates to bool(b)
... print(b[0])
...
10
The following code block demonstrates using a dictionary
key as a condition for the if
statement.
>>> customer = {"name": "John", "member": True}
>>> if customer["member"]:
... print("{} is a premium member".format(customer["name"]))
...
John is a premium member
What's the output of the following code?
>>> a = []
>>> if len(a):
... print("We can use objects as conditions")
... else:
... print("This shouldn't be printed")
...
- This shouldn't be printed
- Raises Error.
- We can use objects as conditions
- Doesn't print anything
To solve the previous exercise, we will recall that Python evaluates 0
as False
.
False
?
Python evaluates empty containers such as sets
, dictionaries
, and lists
as False
. So far, we have seen a single condition for an if-else
statement.
True
simultaneously?
We can chain multiple conditions using logical operators to create complicated condition tests and control flow. We can also nest if-else
statements within one another. Let's take an example to understand more.
In mathematics, the triangle inequality theorem states that a triangle is valid if the sum of two sides is higher than the third side.
$$
\text{For a triangle with sides a, b and c to exists, the following conditions must be met.} \\
a + b > c \\
c + a > b \\
b + c > a \\
\text{where a, b, and c are non-negative integers}
$$
Let's write a function that checks if three sides can be a side of a triangle. It accepts sides of a triangle as three positional arguments a,b and c
and returns whether these line-segments can form a triangle or not.
>>> def check_sides(a, b, c):
# Checks if sides are non-zero negative integers
if all((a > 0, b > 0, c > 0)):
# Triangle Inequality Theorem
if a + b > c and a + c > b and c + b > a:
print("{}, {} and {} can form a triangle"
.format(a, b, c))
else:
print("{}, {} and {} cannot form a triangle"
.format(a, b, c))
else:
print("Sides of a triangle cannot be negative or zero")
In the above code listing, we can see that the two if
statements are nested. The outer if
statement checks if all the sides are greater than 0
. Simultaneously, the inner if
statement checks if the sum of each of the two sides is greater than the third side.
Let's test our newly created function.
>>> check_sides(3, 4, 5)
3, 4 and 5 can form a triangle
>>> check_sides(10, 20, 300)
10, 20 and 300 cannot form a triangle
>>> check_sides(10, -20, 300)
Sides of a triangle cannot be negative or zero
Our function check_sides()
is working as expected.
What's the output of the following script?
a, b = (), ()
c, d = [], []
if a is b:
if c is d:
d.append(1)
else:
d.append(2)
else:
d.append(3)
print(d)
[1, 2]
[3]
[2]
[]
Ternary expression
There is a short form of if
statement to write a conditional expression in a single statement. It's called Ternary expression.
Ternary expression in Python is equivalent to ternary operation found in the C
programming language. The following is the syntax of the compact if
statement, also known as ternary expression:
[on_true] if [boolean_expression] else [on_false]
The ternary form requires
- a
boolean_expression
on_true
value which is returned if the Boolean expression returnsTrue
on_false
value which is returned if the Boolean expression returnsFalse
For example.
>>> a, b, = 10, 30
>>> smaller = a if a < b else b # 10
Here,
on_true
value :a
on_false
value :b
boolean_expression
:a < b
Unlike the earlier if-else statements, we cannot use the elif
keyword cannot. You can use the ternary if
statement to execute simple one-line expressions conditionally.
We can chain a couple of ternary if
statements to create the same effect as nested if
statements.
>>> a, b, c = 4, 2, 5
# Returns the largest of all three
>>> a if a > b and a > c else b if b > c else c
5
However, for your sanity and others, avoid using multiple ternary expressions in the same line.
What's the output of the following script?
if 1 if 1 < 2 else 2 :
print("Even this works")
else:
print("Not really")
- Not really
- Even this works
- Raises
SyntaxError
- Raises
ValueError
This brings us to the end of the conditional execution.
Python executes the code block in an if
statement once. Python also contains control-flow statements that can execute a group of statements repeatedly. We call these control flow statements as loops. We will learn more about loops in detail in the next section.
Loops
In programming, to repeatedly execute a group of statements, a loop statement is used. In Python, we use for
and while
keywords to implement loops in Python.
A loop is a shape that bends around and crosses itself. Let's learn our first loop - the while
loop.
While Loop
The while
statement creates a loop in Python using the following syntax.
# Until the condition evaluates to `True`
while condition:
# Execute this code block

Like the if
statement, the' while' statement has a condition associated with it. Python executes the code block inside the while
statement until the condition evaluates False
.
>>> counter = 5 # Assign name counter to 5
>>> while counter :
... print(counter)
... counter -= 1 # Decrease the value of name by 1
...
5
4
3
2
1
In the above code block, the condition provided to the while statement is the object referenced by the name counter
. As we might recall, most numbers' truth value is True
, except 0
.
After completing the code block's execution inside the while
, Python checks if the condition associated still evaluates True
. Until the condition evaluates to False
, Python continuously executes the code block.
A single execution of the code block inside the while
loop is called an iteration.
In the above code block, to make the condition evaluate to False
, we decrease the value referenced by the name counter
by one each time it executes the code block. At some point, the name counter
's value becomes 0
, which has a truth value of False
, and thus Python exits the loop.
False
. What do you think happens when the condition doesn't evaluate to False
?
If you don't make provisions to make the condition to become False
eventually, Python will execute the code block inside the while loop indefinitely. In programming, a loop code block that goes on endlessly is called an infinite loop.
We can remove the statement counter -= 1
to make the code an infinite loop.
Before implementing a while
loop, you should ensure that the condition eventually evaluates False
. Python can't determine if a condition will subsequently become False
or not.
If you executed an infinite loop, it might cause the console or terminal to become unresponsive. Then you can close an unresponsive console or terminal directly by usually (Ctrl + c
or Cmd + c
).
Is the following code an example of an infinite loop?
>>> grocery_list = ["Apples", "Oranges", "Bananas", "Soda"]
>>> while grocery_list:
... print(grocery_list.pop())
...
- Yes, this is an infinite loop.
- No, this is not a loop.
Earlier, we saw that the list object has a method s.pop()
, which retrieves a randomly selected element and removes it from the list. We also know that the truth value of an empty list is False
. Equipped with these two sets of information, we can see that the previous exercises' code isn't an infinite loop.
The output of the code in the above exercise is as follows:
>>> grocery_list = ["Apples", "Oranges", "Bananas", "Soda"]
>>> while grocery_list:
... print(grocery_list.pop())
...
Soda
Bananas
Oranges
Apples
In the above code listing, after the grocery_list
list become empty, it evaluates to False
, and Python exits out of the while
loop.
grocery_list
becomes empty due to using the pop()
method. Can you think of a way to access each element without modifying the list element itself?
We can define the name counter as the condition for the while loop and then use it as an index to access each element of a list or sequence. Let's check an example.
Take a look at the script below.
grocery_list = ["Apples", "Oranges", "Bananas", "Soda"]
counter = len(grocery_list) - 1 # Index starts from 0
print("The following items are on my grocery list:")
while counter + 1:
print(grocery_list[counter]) # Access the item using `counter` as index
counter -= 1
We can check out the output of the code below.
>>> python3 grocery.py
The following items are on my grocery list:
Soda
Bananas
Oranges
Apples
As you can see, we can initialize a counter to iterate or loop through all the items in a list. The while
loop condition is counter + 1
instead of counter
. Otherwise, it would skip over the item at the 0
index.
Present below is a Python script.
guests = ["Luffy", "Zorro", "Chopper", "Sanji"]
counter = len(guests)
while counter:
print(f'{guests[counter-1]} just arrived !')
counter -= 1
Which of the following statement is not printed as output?
Sanji just arrived !
Chopper just arrived !
Luffy just arrived !
All the above statements are printed
while
loop. But what about containers that don't support accessing items using indexing such as sets
and dictionary
? Can you take a guess how do we iterate over elements in sets
and dictionary
?
We can access the elements of sets and dictionaries using for
loops. Let's understand in detail about for
loops.
For Loops
Objects that support iteration are said to be iterable.
This definition is not much helpful. Let's try another definition of iterables.
An iterable is any Python object capable of returning its members one at a time, permitting it to be iterated over in a for
loop.
That's much better. Now, we can start with understanding for
loops.
The for
statement iterates over an iterable until there are no more elements available.

We can see the general syntax of a for
loop below.
for i in iterable :
# Execute this code block
In the statement, the name i
is often referred to as iteration variable. With each iteration, the iteration variable is assigned a new value provided by the iterable. You can think of it as the iterable returning one of the members assigned to the iteration variable i
for a while. Let's look at an example.
>>> a = [1, 2, 3, 4, 5]
>>> for i in a:
... print(i)
...
1
2
3
4
5
In the above code sample, the for loop iterates over the list a. Python first assigns the iteration variable i
to the value 1
, which is the first item occupied in the list a
in the first iteration. In the next iteration, it is assigned the next item in the list, and so forth.
Below is a python script.
person = { "name" : "John Doe", "age" : "16", "country" : "Bhutan" }
for detail in person:
print(f'{detail.title()} : {person[detail]}')
Which of the following statement will be printed upon executing the above script?
Country: Bhutan
Bhutan: Country
country: Bhutan
Raises NameError: name 'detail' is not defined.
The iteration variable in a for
loop can be assigned any valid name. It doesn't necessarily need to be i
. You can use this to make your code more readable.
Let's look at a couple of more things about the iteration variable.
- The scope of the iteration variable is not private to the
for
loop statement. If another name exists with the same name as the iteration variable, Python overwrites it. - The last value assigned to the iteration variable will remain attached even after loop completion.
We can check this by an example.
>>> grocery_list = ["Apples", "Oranges", "Bananas", "Soda"]
>>> item = "Nachos"
>>> for item in grocery_list:
... print(item)
...
Apples
Oranges
Bananas
Soda
>>> item
'Soda' # Last item of the list
You can see that the name item
assigned to the string Nachos
is reassigned to the last element of the list after the iteration.
while
loop, we can define a counter variable and use it to iterate over a sequence. Can we do the same index-based looping in for
loops?
As for
loops take an iterable object, we need to define our sequence of numbers first before we can loop over it. To iterate over a sequence, the built-in function range()
can be used to generate a sequence of numbers corresponding to the length of the sequence to use index-based looping.
Let's look at range()
function next.
Range
The function signature of range()
function is range(start, stop, step)
where the start
and step
arguments are optional. The range function generates a produces a sequence of numbers from and including the start
parameter to the stop
parameter. The start argument defaults to 0
while the step parameter defaults to 1
. For instance,
>>> range(9) # same as range(0, 9, 1)
range(0, 9)
You can view the sequence generated by the range()
by converting it to a list or tuple.
>>> tuple(range(9))
(0, 1, 2, 3, 4, 5, 6, 7, 8)
The range()
function returns a <class 'range'>
object, a type of iterable that can be used for iteration.
>>> for i in range(10, 50, 5):
... print(i)
...
10
15
20
25
30
35
40
45
Which of the statement is printed out due to the execution of the following script:
alphabets = 'ABCDEFG'
counter = 1
for letter in alphabets:
print(counter, ":", letter)
counter += 1
7 : G
A : 1
1 : G
F: 4
As shown in the above exercise, you often will require the element's position index in an iterable along with the element value.
Many programming languages do this by keeping a separate variable to keep track of the iteration count and increasing it by one during each iteration. We can understand it better using the code listing.
The format of using an iteration count to keep track of the position index of the iterable is shown below:
index = 0
for a in s:
# Code block
index += 1
The name index
acts as a counter that stores the current iteration value in the above code. Keeping a counter to track the position of the element in the iteration is a common programming paradigm. Python provides a built-in function enumerate()
for the same.
Enumerate
The built-in enumerate()
which provides a tuple of pair values containing, (index, iterable[index])
. We can understand it better in the code listing below.
>>> grocery_list = ["Apples", "Oranges", "Bananas", "Soda"]
# i is a tuple (index, grocery_list[index])
>>> for i in enumerate(grocery_list):
... print(i)
...
(0, 'Apples')
(1, 'Oranges')
(2, 'Bananas')
(3, 'Soda')
The enumerate()
function also accepts an optional argument start
, which defaults to 0
and can be used to give a user-defined starting index. We can even unpack the tuple using two iteration variables.
>>> for index, val in enumerate(grocery_list, 1):
... print("{}. {}".format(index, val))
...
1. Apples
2. Oranges
3. Bananas
4. Soda
The enumerate()
function is a useful function for creating an indexed list.
What is the value of the following operation?
>>> person = {'name': "John", "age": 16 }
>>> list(enumerate(a))
[(1,'John'), (2, 16)]
[(0, 'name'), (1, 'age')]
[(1, 'name'), (2, 'age')]
[(0, 'John'), (1, 16)]
You can convert the enumerate(<iterable>)
object into sequences such as lists or tuples using their respective constructors.
Often, you need to iterate over two or more iterables or sequences in parallel. Let's take a look at how we can do this in Python.
Zip
Suppose you have three separate lists containing a person's name
, city
, and the food
they prefer.
| Name | City | Food |
| | -- | - |
| Thomas Sowell | Barcelona | Tacos |
| Li Xiu | Tokyo | Noodles |
| Joe Doe | Pondicherry | Dosa |
| Dark Puckerberg | Melbourne | Bear |
We wish to generate a print all of the person's details in a single string. We can do that by simultaneously looping over three lists and iterating over items using a while
loop. Let's see this in action.
>>> names = ["Thomas Sowell", "Li Xiu",
"Joe Doe", "Dark Puckerberg"]
>>> city, food = ["Barcelona", "Tokyo",
"Pondicherry", "Melbourne"],
["Tacos", "Noodles",
"Dosa", "Bear"]
>>> i = 0
# Till `i` equals the shortest of three lists
>>> while i < min(len(names),len(city), len(food)):
print("{}. {} from {} likes {}.".format(i+1, names[i], city[i], food[i]))
i += 1
1. Thomas Sowell from Barcelona likes Tacos.
2. Li Xiu from Tokyo likes Noodles.
3. Joe Doe from Pondicherry likes Dosa.
4. Dark Puckerberg from Melbourne likes Bear.
In the above code example, we are iterating for three lists simultaneously and increasing our iteration variable i
by 1
in each iteration.
This pattern of iterating over two or more lists in pretty common in programming, so Python provides a built-in function zip()
to merge two or more lists to form a list of tuples. The above identifiers: names
, city
, and food
can be zipped together to form a new object containing the tuple of objects from each list. For instance,
>>> zip(names, city, food) # Returns a zip iterable
<zip at 0x7f5eaf293d48>
>>> list(zip(names, city, food)) # Convert the zip object into a list
[('Thomas Sowell', 'Barcelona', 'Tacos'),
('Li Xiu', 'Tokyo', 'Noodles'),
('Joe Doe', 'Pondicherry', 'Dosa'),
('Dark Puckerberg', 'Melbourne', 'Bear')]
We can rewrite the above while loop using the zip()
function in the following way.
>>> for name, city_name, food_name in zip(names, city, food):
print("{} from {} likes {}".format(name, city_name, food_name))
Thomas Sowell from Barcelona likes Tacos
Li Xiu from Tokyo likes Noodles
Joe Doe from Pondicherry likes Dosa
Dark Puckerberg from Melbourne likes Bear
Continuing from the above names, what's the __A__
stands for in the following code listing?
>>> for ____A____ in list(enumerate(zip(names, city, food))):
print("{}. {} from {} likes {}".format(idx+1, name, city_name, food_name))
1. Thomas Sowell from Barcelona likes Tacos
2. Li Xiu from Tokyo likes Noodles
3. Joe Doe from Pondicherry likes Dosa
4. Dark Puckerberg from Melbourne likes Bear
idx, (name, city_name, food_name)
index, (name, city_name, food_name)
[idx, (name, city_name,food_name)]
idx, name, city_name, food_name
To include the index, we can use the enumerate()
function after zipping the function. Note that the enumerate()
creates a nested tuple; therefore, we need to unpack the nested tuple.
When you zip two or more iterables, it creates a list of tuples. Let's understand the previous exercise in detail.
>>> names = ["Thomas Sowell", "Li Xiu", "Joe Doe", "Dark Puckerberg"]
>>> city = ["Barcelona", "Tokyo", "Pondicherry", "Melbourne"]
>>> food = ["Tacos", "Noodles", "Dosa", "Bear"]
When we zip the above lists, we get the following.
>>> list(zip(names, city, food))
[('Thomas Sowell', 'Barcelona', 'Tacos'), ('Li Xiu', 'Tokyo', 'Noodles'), ('Joe Doe', 'Pondicherry', 'Dosa'), ('Dark Puckerberg', 'Melbourne', 'Bear')]
If we need the index, we can use the enumerate()
function on top of the zip()
.
>>> list(enumerate(zip(names, city, food)))
[(0, ('Thomas Sowell', 'Barcelona', 'Tacos')),
(1, ('Li Xiu', 'Tokyo', 'Noodles')),
(2, ('Joe Doe', 'Pondicherry', 'Dosa')),
(3, ('Dark Puckerberg', 'Melbourne', 'Bear'))]
You can notice that each item is of the nested tuple format, (index, (names, city, food))
.
When we want to iterate over the nested tuple, we need to unpack the tuple first.
>>> for index, (name, city_name, food_name) in list(enumerate(zip(names, city, food))): # Unpacking nested tuple
print(f"{index}. {name} from {city_name} likes {food_name}")
1. Thomas Sowell from Barcelona likes Tacos
2. Li Xiu from Tokyo likes Noodles
3. Joe Doe from Pondicherry likes Dosa
4. Dark Puckerberg from Melbourne likes Bear
What's the output of the following code?
>>> for index, num in enumerate(range(9)):
... print(list(range(index, num)))
...
- 9 empty lists
- 9 lists with an increasing number of elements
- 9 lists with a decreasing number of elements
- Raises an error
Break Statement
We can break out of a loop in the middle of an ongoing iteration using the break
keyword.
Thebreak
statement exits out of the innermostfor
orwhile
loop.
The break
statement prematurely exits the loop, and the rest of the loop doesn't iterate over the remaining iterables.
For instance,
>>> guest_names = ["Adam", "James", "Sid", "David", "Harry", "Tyrion", "Lufy"]
>>> for idx, name in enumerate(guest_names):
... if name == "Sid":
... print("Wait, why is Sid invited?")
... break
... print("{} is invited".format(name))
...
Adam is invited
James is invited
Wait, why is Sid invited?
The Python stops looping over the guest_names
iterable in the above code listing when it is the string object Sid
. After the loop exits, Python doesn't iterate over the items after the Sid
anymore.
For nested loops, the break
statement breaks out of the innermost loop. Let's write a nested loop to get the prime numbers in the first 20
numbers to see this in action.
not_prime = set()
for n in range(2, 20):
for x in range(2, n):
if n % x == 0:
not_prime.add(n)
break
prime_numbers = set(range(2, 20)) - not_prime
print(prime_numbers)
The output of the prime_20.py
is shown below:
{2, 3, 5, 7, 11, 13, 17, 19}
prime_20.py
?
In mathematics, a prime number is a number that is divisible only by one and the number itself. So, all we need to check is if a number is divisible by any number lower than itself.
In the above code listing, for every number between [2, 20)
, we are checking if it is divisible by a number other than one and itself. We can do this in the following way:
- For a number n between
2
and20
, check if there exists a numberx
in therange(2, n)
, which perfectly divides the number. - If we find such a number
x
, the numbern
is added to the setnot_prime
, and the innermost loop exits using thebreak
statement and continues the iteration forn+1
. - After the iteration is complete, we can subtract the set of numbers in the
range(2, 20)
from the setnot_prime
. - The new set is assigned the name
prime_numbers
.
Reorder the code according to the code written in prime_20.py
.
-
Loop over
range(2,20)
with iteration variablen
- Create an empty set with the name
not_prime
. -
An
if
condition check ifn
is divisible byx
-
Loop over over
range(2, n)
with iteration variablex
. -
Return the difference between the set of numbers in the
range(2, 20)
andnot_prime
as prime_numbers. -
If
n
is divisible byx
, addn
to the setnot_prime
andbreak
.
Continue Statement
To jump to the next iteration in a loop, we can use the continue
keyword. The continue keyword skips the remainder of the loop body. Let's say we want to skip executing the code-block for a particular item in a list. We can put it in an if
condition along-with a continue
statement.
For example, in the below code example, we wish to skip printing the string object Sid
.
>>> guest_names = ["Adam", "James", "Sid", "David", "Harry", "Tyrion", "Luffy"]
>>> for idx, name in enumerate(guest_names):
... if name == "Sid": # Ignore Sid
... continue
... print("{}.Inviting {}".format(idx+1, name))
...
1.Inviting Adam
2.Inviting James
4.Inviting David
5.Inviting Harry
6.Inviting Tyrion
7.Inviting Luffy
The loop iterates over the guest_names
list and prints a string in the above code listing. In the loop, the if
statement checks if the name
iteration variable equals Sid
, in which case, the execution of the remainder of the loop code is skipped, and iteration continues for the next item in the iterable. You can also notice that the index 3
is missing from the printed statements.
Which of the following numbers will be present in the following script's output?
for num in range(10, 20):
if not num % 3:
continue
else:
print(num)
10
12
15
20
Loops with else
clause
Like if statements, loops can also have an else
clause. We can use the else
clause in loops in the following situations:
for
loop terminates through exhaustion of the iterable- The condition associated with the
while
loop becomes fromTrue
toFalse
- The loop wouldn't execute at all
Python doesn't execute the else clause when a break statement terminates a loop.
We can see each of the cases in the following code listing.
for x in []: # Loop 1
print("Loop 1")
else:
print("Else statement executed for loop 1")
for x in [1]: # Loop 2
print("Loop 2")
else:
print("Else statement executed for loop 2")
for x in range(2): # Loop 3
print("Loop 3")
if x == 0:
break
else:
print("Else statement executed for loop 3")
The output of for_else.py
is shown below:
Else statement executed for loop 1
Loop 2
Else statement executed for loop 2
Loop 3
In the above code sample, the else
statements get executed for Loop 1 and 2 but not for 3 as loop three terminates through the break
statement.
Is the else clause executed in the following script?
guest_names = ["Adam", "James", "David", "Harry", "Tyrion", "Luffy"]
for idx, name in enumerate(guest_names):
if name == "Sid":
print("Wait, why is Sid invited !")
break
print(f"{name} is invited")
else:
print("Sid is not invited")
- Yes, it is.
- No, it isn't.
As the string object Sid
isn't present in the list guest_name
, the break
statement is never executed. Therefore, the else clause is executed.
Let's take a look at a while
loop with an else
clause. In the script below, there are three different while
loops.
- first one, where the condition is
False
; therefore, the code block isn't executed, but theelse
is executed. - second one, where the condition is initially
True
but then becomesFalse
, and theelse
clause is executed. - third one, where the code block exits using a
break
statement, and theelse
clause is not executed.
i = 1; j = 2 # For loop 2 & 3
while 0: # Loop 1
print("Loop # 1")
else:
print("Else statement executed for loop 1")
while i: # Loop 2
print("Loop # 2"); i -= 1
else:
print("Else statement executed for loop 2")
while j: # Loop 3
print("Loop # 3"); j -= 1
if j == 1:
break
else:
print("Else statement executed for loop 3")
The output of the while_else.py
i
s shown below:
Else statement executed for loop 1
Loop 2
Else statement executed for loop 2
Loop 3
In the python script in file6.py
, Loop 2 and 3 execute while the else statement for Loop 1 and 2 executes. This is similar to the results we got for for
loops.
This comes to the end of loops in Python. In the next section, we will cover the call stack in Python.
Call Stack
We have seen that Python executes the statements in a program from top to bottom unless the statements involve control-flow constructs such as loops and if-else
statements.
This is not a complete description of how Python executes the program. It is missing out on details about function calls.
A function body is not executed until it is called.
Each time you call a function, Python takes a detour from the execution flow to go to that function code's function body. Let's do a small exercise.
Take, for instance, the following script.
def foo(): #1
print("Line 2") #2
#3
def bar(): #4
foo() #5
print("Line 6") #6
foo() #7
#8
def foobar(): #9
bar() #10
foo() #11
print("Line 12") #12
#13
print("Line 14") #14
foobar() #15
What's the output when you run the above program?
- Line 14
- Line 2
- Line 6
- Line 2
- Line 2
- Line 12
From the previous exercise, we can see that Python doesn't seem to execute the statements sequentially. Let's take a deeper look at the code listing in the previous exercise to understand more.
The output of the above program is as follows:
Line 14
Line 2
Line 6
Line 2
Line 2
Line 12
Python executes the script using the following heuristic:
- Execute the first line that is not part of a function definition
- After executing the line, move on to the next line of code
- If the next line of code is part of a control statement such as
if-else
, loops, function calls, or function returns, Python executes them differently.
Earlier, we saw how control statements affect the flow of executions in Python. Now, we will focus on understanding how function calls and function returns are executed.
- When Python encounters a function call, it jumps from that line of code to the first line of code inside the function definition.
- When Python returns from a function call, it continues after the line, which invoked the function call that sent it to the function.
Therefore, Python executes the above script in the following way:
- Executes the
print
statement in line 14 - Calls the function
foobar()
at line 15 - Goes to line 10 and calls the
bar()
function - Goes to line 5 and calls the
foo()
function - Executes the
print
statement in line 2 - Returns from
foo()
to line 5 - Executes the line
print
statement in line 6 - Calls the
foo()
function at line 7 - Executes the
print
statement in line 2 - Returns from
foo()
to line 7 - Returns to line 10
- Call the
foo()
function at line 11 - Executes the
print
statement in line 2 - Returns from
foo()
to line 11 - Executes the
print
statement in line 12 - Returns to line 15
- End of Program
At this point, you might be wondering, how does Python keep track of where to jump when it returns from a function or how does it picks up from where it left.
To understand how Python keeps track of function calls, we first need to understand something called the stack.
Stack
A stack is an ordered collection of items where new items and removal of existing items always occur at the same end.
In such ordering, the most recent item to add is the one that is in a position to be removed first. This ordering principle is called LIFO( last-in, first-out). Newer items are near the top, while the older items are near the base of the stack.
Many examples of stacks occur in our everyday world. A concrete example of a stack will be a tennis balls container with only one side open. In our tennis ball container, new balls can be added only from one end, while the last item added will be removed first.
We can implement a stack in Python using a list object in Python if we only use the append()
method to add items to the pop()
method to retrieve and remove an element from the list. Let's create a list object called the ball_holder
.
>>> ball_holder = []
For the list object ball_holder
to function like a stack, we can use only two of its methods:
append()
method to add elementspop()
method to retrieve and remove elements
>>> ball_holder = [] # []
>>> ball_holder.append('Ball 1') # ['Ball 1']
>>> ball_holder.append('Ball 2') # ['Ball 1', 'Ball 2']
>>> ball_holder.append('Ball 3') # ['Ball 1', 'Ball 2', 'Ball 3']
>>> ball_holder.pop() # ['Ball 1', 'Ball 2']
>>> ball_holder.append('Ball 4') # ['Ball 1', 'Ball 2', 'Ball 4']
>>> ball_holder.pop() # ['Ball 1', 'Ball 2']
>>> ball_holder.pop() # ['Ball 1']
>>> ball_holder.pop() # []
We can visualize the tennis ball stack using the following diagram.
Now that we have some idea about stacks let's see how Python executes a function call.
What does the output of the following code listing look like?
ball_holder = ['Ball 1']
ball_holder.append('Ball 2')
ball_holder.append('Ball 4')
ball_holder.append('Ball 3')
print(ball_holder.pop())
print(ball_holder.pop())
ball_holder.append('Ball 5')
print(ball_holder.pop())
ball_holder.append('Ball 6')
ball_holder.append('Ball 7')
print(ball_holder.pop())
ball_holder.append('Ball 8')
print(ball_holder.pop())
- Ball 3
- Ball 8
- Ball 5
- Ball 7
- Ball 4
Now that we have some idea about stacks, let's look at how Python executes a function call.
Call Stack
The Python interpreter uses a stack to run a Python program. The stack is commonly referred to as call stack or run-time stack or stack. The call stack consists of call frames which store information about each function call. A call frame is how Python keeps track of execution while a function call progresses. The call stack works in the following way:
Whenever a function is called in Python, a new call frame is pushed onto the call stack.
Every time the function call returns, its call frame is popped or deleted from the call stack.
The module in which the program runs has the bottom-most frame. It is called the global frame or the module frame. Let's write another script to understand call frames.
def foo():
print("Last")
def bar():
print("Second)
def foobar():
bar()
foo()
print("First")
foobar()
The above script foobar.py
returns the following output.
First
Second
Last
We can visualize the way Python executes the script using the following diagram.
foobar.py
Let's take our previous script.
def foo():
print("Last")
def bar():
print("Second)
def foobar():
bar()
foo()
print("First")
foobar()
Reorder the statements according to which Python executes them.
- Goes into the first line of
bar()
function body - Executes
print("First")
- Executes
print("Last")
- Returns from the
foo()
function body - Returns from the
foobar()
function body - Executes
print("Second")
- Returns from the
bar()
function - Goes into the first line of
foo()
function body - Goes into the first line of
foobar()
function body
As we can see, Python adds call frames to the call stack whenever it encounters a function call.
After the function call returns, Python removes the frame from the call stack and adds the next function call into the call stack. Figure 7 shows snapshots of the call stack during the execution above.
In the figure 7, you can see how the frames are added and removed after their execution.
Below is a code listing which we encountered earlier.
def foo():
print("Foo")
def foo_foo():
print("Foo-Foo") # Curently Executing
def bar():
foo()
foo_foo()
def foobar():
bar()
foo()
foobar()
Python is currently executing the line 6 ( print("Line 6")
). Can you reorder the below frames to resemble the call stack?
Assume that the bottom-most frame is the first call frame to be added to the call stack.
print("Foo-Foo")
Framebar()
Framefoo_foo()
Framefoobar()
Frame- Module Frame
Call Frames
The call frames contains information such as the local namespace and variables inside a function. To understand, let's take another example.
We will create a script that solves a quadratic equation of form $ax^2 + bx + c$. The discriminant method can calculate the solution of the two roots of a quadratic equation. The following shows the formulae for obtaining a quadratic equation's roots using discriminant($\Delta$).
$$
\text{Roots of equation } = \frac{-b \pm \sqrt(\Delta)}{2a} \
\text{where } \Delta = b^2 - 4ac \
$$
Let's say we want to solve the equation.
$$
x^2 + 2x + 1 = 0
$$
We can do so using the following script.
Note that we have to use thecmath
module instead ofmath
as the roots can be imaginary.
# Solve the quadratic equation
# ax**2 + bx + c = 0
# using discriminant method
import cmath # Roots can be complex
a = 1
b = 2
c = 1
def quadratic_solver(a,b,c):
# calculate the discriminant
d = (b**2) - (4*a*c)
# find two solutions using disrciminant
x1 = (-b-cmath.sqrt(d))/(2*a)
x2 = (-b+cmath.sqrt(d))/(2*a)
return (x1, x2)
sol_1, sol_2 = quadratic_solver(a, b, c)
print(f'The solution is {sol_1} and {sol_2}')
The output of quadratic.py
is shown below:
The solution is (-1+0j) and (-1+0j)
quadratic.py
script work?
In the above script, we define the quadratic equation's coefficients and provide it to the quadratic_solver()
function. The function calculates the solution using the discriminant method and returns a tuple of two solutions. Let's look at how Python executes the above script.
The diagram above depicts the stack diagram for the quadratic equation solver script. You can notice the following:
- The
module
frame contains the global names such asa, b, c, cmath
- While the
quadratic_solver()
frame is executed, its call frame is added on top of themodule
frame. - The
quadratic_solver()
frame contains the local variables sucha, b, c, d, x1, x2
. - After the
quadratic_solver()
returns, it's call frame and the information it contained is removed from the stack.
The advantage of using the stack to store data is that memory is automatically managed for you. But the size of the stack is limited or finite. At some point, you are going to run out of space to hold additional call frames. This problem is often faced when you recursively call a function. Let's understand more in the next section.

Recursion
Recursion occurs when a thing is defined in terms of itself. Recursive definitions are self-referential.
For instance, in his book Gödel, Escher, Bach, Douglas Hofstader gives an interesting example of a recursive definition.
Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law.
In programming, recursion is one of the most interesting problem-solving methods.
The word recursion has roots in the Latin word recursiō, which means the act of running back or again or return. In programming, we define a recursive function as follows:
A function is said to be recursive when it calls itself from within its code definition.
When you call a function inside its definition, it becomes a recursive function. Let's look at one of such recursive functions below.
def recur_func():
recur_func()
recur_func()
recur_func()
to invoke the function itself. What do you think happens when you execute the above program?
You might have guessed that Python encounters an error while executing the script.
The Python interpreter's call stack is finite and can store a limited number of call frames. When the stack is full, and you attempt to add additional frames, it is called stack overflow.
Stack overflow means that a Python program has run out of memory to hold the frames in the call stack.
When the Python Interpreter experiences stack overflow, it causes the interpreter to crash or freeze. To avoid the stack overflow, Python sets a limit for the total number of frames that the call stack can hold. This limit is called the recursion limit. It is usually much lower than the actual number that can cause a stack overflow.
You can find out the recursion limit using the getrecursionlimit()
function from the sys
module.
>>> import sys
>>> sys.getrecursionlimit()
1000
Usually, Python sets the limit is 1000 (the maximum number of frames that the call stack can hold). However, it might differ for different Python interpreters. You can set the recursion limit of your Python interpreter to any other number you like using the setrecursionlimit()
function from the sys
module. Although, if you set the number higher than the actual frames the interpreter call stack can hold, it might cause Python to crash while executing a recursive function.
When we try to execute recur_fun()
, the number of call frames hit the recursion limit.
>>> def recur_func():
... recur_func()
>>> recur_func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in recursive_function
File "<stdin>", line 2, in recursive_function
File "<stdin>", line 2, in recursive_function
[Previous line repeated 996 more times]
RecursionError: maximum recursion dep3th exceeded
While executing the above script, Python invokes recur_func()
repeatedly due to the function's recursive nature. As a result of the repeated invocation, the interpreter call stack is filled with recur_func()
call frames till the recursion limit. Any additional invocation of recur_func()
results in Python raising RecursionError
.
Recursion Error is raised when the call frames' number in the call stack is equal to the recursion limit. Python attempts to add another call frame to the stack.
We can visualize the execution using the below stack diagram.
In the above diagram, we can see that the module
frame occupies the bottom-most frame while the rest 999
frames are occupied by the recur_func()
frame. Adding another recur_func()
call frame causes Python to raise RecursionError
.
RecursionError
. How can we ensure that recursive functions don't raise RecursionError
?
While working with recursive functions, we need to think about breaking off the recursion. We can do so by organizing the recursive function definition inside an if-else
statement.
We can rewrite our previous recur_func()
as follows:
def recur_func(count=0):
count = count + 1
if count < 5:
print(f"Recursion Count: {count}")
recur_func(count)
else:
print("Recursion Completed")
recur_func()
We get the following by executing the script.
Recursion Count: 1
Recursion Count: 2
Recursion Count: 3
Recursion Count: 4
Recursion Completed
While using recursion as a problem-solving method, the general idea is to think about the ending first. In the updated version of recur_func()
, we use a count
object to keep track of the recursion along with an if
statement. This ensures that the function doesn't repeatedly call itself indefinitely and eventually returns from the function call.
At this point, you might be wondering if there is any actual use of recursion in programming. There are many instances where recursion proves to be a practical approach. In the next section, we will take a look at one such problem: Factorial.
Factorial
The factorial of a given non-negative number $n$ is the product of all the natural numbers between $1$ and $n$.
The factorial of a negative number doesn't exist, and the factorial of $0$ is $1$. Mathematically it is shown in the below equation.
$$
\text{Factorial of a natural number } n \text{ is } \
n! = \prod_1^nn
$$
From the equation, we can think of factorial as follows:
$$
4! = 4\times3\times2\times1 = 24 \\
5! = 5\times4\times3\times2\times1 = 120 \\
6! = 6\times5\times4\times3\times2\times1 = 720
$$
That's pretty much everything that you need to know about factorials right now. The code below shows a function that accepts a given number and returns it's factorial.
def factorial(n):
result = 1
for num in range(1, n + 1):
result *= num
return result
Let's check out the values of the interpreter.
>>> factorial(4)
24
>>> factorial(5)
120
>>> factorial(6)
720
factorial
function works?
The factorial(n)
function uses loops over a range of (1, n+1)
. In each iteration, it uses the name result
to store the natural numbers' product. After completing the loop, it returns the name result
as the factorial of the number.
The above solution for obtaining the factorial works fine. However, we can use recursion to solve the problem.
The first thing to create a recursive function is that we have to represent the function in terms of itself.
Let's take a look at the factorials once more. In the formula for obtaining the factorial of a number, you can notice the following:
$$
6! = 6\times5! \\
5! = 5\times4! \\
4! = 4\times3! \\
3! = 2\times1! \\
1! = 1\times0! \\
0! = 1
$$
This indicates that,
the factorial of a number is equal to the product of the number and the factorial of the preceding number.
Mathematically, we can state the following:
$$
\text{For a natural number, }\\
n! = n\times(n-1)!
$$
If we think about this with respect out function factorial()
, we can write:
$$
\text{factorial}(n) =
\begin{cases}
n\times\text{factorial}(n-1), & \text{if $n$ $\geq$ 1} \\
1, & \text{if $n$ = 0}
\end{cases}
$$
You can see that we have successfully defined the factorial()
function in terms of itself. Now, let's rewrite the factorial()
.
def factorial(n):
if n == 0:
return 1
return n*factorial(n-1)
You can notice that we are calling the factorial()
inside its definition. This is a recursive function. To ensure that the recursion doesn't go on forever, we have set the limit condition at n = 0
, which returns 1
.
Let's test out our newly written factorial()
function in the interpreter.
>>> factorial(4)
24
>>> factorial(5)
120
>>> factorial(6)
720
As you can see, the function gives the output as expected.
factorial
function works?
When you invoke the factorial()
function with the argument n
, it first checks if the n = 1
. If not, it calls the factorial()
function again, but this time with the argument n-1
. It does this until the function argument is 0
, upon which it returns 1.
We can understand the way our above code works using the stack diagram.
Because we are using recursion inside the function, we are always at risk of exceeding the call stack frames set by Python. This might not be significant for lower values of n
, but it indeed becomes problematic for higher values.
For instance, if your interpreter has the recursion limit set at 1000
, the factorial function above will work for n = 996
. Still, it will not work for n >= 997
. If you try to test out the factorial for 998, Python might give the following output.
>>> factorial(998)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in factorial
File "<stdin>", line 4, in factorial
File "<stdin>", line 4, in factorial
[Previous line repeated 995 more times]
File "<stdin>", line 2, in factorial
RecursionError: maximum recursion depth exceeded in comparison
You can set the recursion limit to a higher number, using the sys.setrecursionlimit()
function to bypass the error. However, doing so is not recommended.
What's the output of the following script?
def func_1(n):
if n == 0:
return 1
return func_1(n-1)**n
print(func_1(6))
- 720
- 120
- 24
- 1
Any value raised to the power of 1 will return 1.
Let's look at another problem that we can solve using recursion: Fibonacci's Sequence.
Fibonacci's Sequence
In mathematics, a sequence starts from 0 and 1 where each number is the sum of the two preceding ones called Fibonacci Sequence. The numbers in the sequence are called Fibonacci Numbers.
The Fibonacci sequence is represented in the following way:
$$
F_n = F_{n-1} + F_{n-2} \\
\text{Given, } F_0 = 0, F_1 = 1 \\
$$
You might recall Leonardo of Pisa, also known as Fibonacci, posthumously. In the 1202 book Liber Abaci, he introduced the world to counting using the Hindu system of counting. In the same book, Fibonacci introduced the sequence to Western European Mathematics.
Note that the sequence had already been described by Indian Mathematician Acharya Pingala in the early 200 BC.
The beginning of the sequence is as follows.
$$
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
$$
If you want to write Fibonacci sequence on your own, you will find yourself doing the following:
$$
\text{Given}, F_0 = 0 \text{ & } F_1 = 1, \\
F_2 = F_1 + F_0 = 1 + 0 = 1 \\
F_3 = F_2 + F_1 = 1 + 1 = 2 \\
F_4 = F_3 + F_2 = 2 + 1 = 3 \\
F_5 = F_4 + F_3 = 3 + 2 = 5
$$
We will write a program that generates the Fibonacci sequence. We are going to approach this problem by breaking it into two functions.
- We will write a function
fib()
that gives thenth
Fibonacci ($F_n$) number. - We will write another function
fib_seq()
that generatesn-length
Fibonacci sequence using thefib()
function
We will focus on writing the fib()
function first. But before that, let's do a small exercise.
What's the output of the following code?
seq = []
(n1, n2) = (0, 1) # initialize first two numbers
seq.append(n1)
next_num = n1 + n2 # sum of two preceding numbers
(n1, n2) = (n2, next_num)
seq.append(n1)
next_num = n1 + n2 # sum of two preceding numbers
(n1, n2) = (n2, next_num)
seq.append(n1)
next_num = n1 + n2 # sum of two preceding numbers
(n1, n2) = (n2, next_num)
seq.append(n1)
next_num = n1 + n2 # sum of two preceding numbers
(n1, n2) = (n2, next_num)
seq.append(n1)
print(seq)
[1, 1, 2, 4]
[1, 1, 3, 5]
[1, 1, 2, 3, 5]
[0, 1, 1, 2, 3]
Take a look at the code in the previous exercise. You will realize that it is generating the Fibonacci sequence itself. In the code, we sum up the previous two terms to get the next_num
and then update n1
and n2
. This is the central part of the nth
Fibonacci number generating algorithm.
We can state the algorithm for generating the nth
Fibonacci number generating as the following:
- Define the first two numbers of the sequence:
n1 = 0, n2 = 1
- The next number in the sequence
next_num
can be obtained by addingn1
andn2.
- Before moving on to get the next number, update the values of
n1
andn2
as follows: n1 = n2
n2 = next_num
- Repeat steps two and 3 to get the next number in the sequence.
Now that we have figured out the algorithm, let's write the nth
Fibonacci number generating function fib()
:
def fib(n):
if n < 0:
print('Please enter a positive integer')
elif n in (0, 1):
return 0 if n == 0 else 1
else:
count = 0
(n1, n2) = (0, 1) # first two terms
while count < n:
# sum of two preceding numbers
next_num = n1 + n2
# update value for the next loop
(n1, n2) = (n2, next_num)
count += 1
return n1
Let's test out our function in the interpreter.
>>> print(fib(2))
1
>>> print(fib(3))
2
>>> print(fib(4))
3
>>> print(fib(5))
5
>>> print(fib(6))
8
As you can see, our function is returning the correct values.
nth
Fibonacci number in the sequence, how can we get the entire n
-length sequence?
As we have already created the fib()
function, we can quickly write a function to generate the $n-length$ Fibonacci sequence.
We can proceed to write a function that generates n-length Fibonacci sequence as follows:
def fib_seq(n):
sequence = []
count = 0
while count < n:
sequence.append(fib(count))
count += 1
print(*sequence) # unpacking the list object
We can unpack a list using the asterisk operator inside the print()
function. Let's try it out on the interpreter.
>>> fib_seq(2)
0 1
>>> fib_seq(3)
0 1 1
>>> fib_seq(4)
0 1 1 2
>>> fib_seq(5)
0 1 1 2 3
>>> fib_seq(6)
0 1 1 2 3 5
As you can see, the fib_seq()
function works as expected.
fib_seq()
function works?
Previously, we rewrote the factorial()
function using recursion. We can do the same for our fib()
function. However, first, we need to understand how to represent the current fib()
in terms of itself.
Mathematically, we can write the following about the Fibonacci numbers:
$$
F_n = F_{n-1} + F_{n-2} \\
\text{where, } F_0 = 0, \text{ } F_1 = 1
$$
If we think about this with respect to our function fib()
, we can write:
$$
\text{fib}(n) =
\begin{cases}
\text{fib}(n-1) + \text{fib}(n-2), & \text{if $n$ $>$ 1} \\
1, & \text{if $n$ =1} \\
0, & \text{if $n$ =0 }
\end{cases}
$$
We have successfully defined the fib()
function in terms of itself.
Now we can rewrite the fib()
function using recursion.
def recur_fibo(n):
if n <= 1:
return n
else:
return(recur_fibo(n-1) + recur_fibo(n-2))
That's it. An elegant solution, isn't it?
Now, let's test out our function in the interpreter.
>>> print(fib(2))
1
>>> print(fib(3))
2
>>> print(fib(4))
3
>>> print(fib(5))
5
>>> print(fib(6))
8
The result is as we expected. If you test out the fib_seq()
function with the recursive fib()
function, you will find that it also works fine like the previous factorial()
function.
Keep the recursion limit in mind while invoking the fib()
function.
While creating a recursive function,
- first, try to represent the function in terms of itself.
- Second, keep the end of the function invocation in mind. Otherwise, you will end up invoking the function indefinitely.
Hopefully, how Python leverages stacks to execute function is clear to you now. Incidentally, call stacks are also helpful while tracking errors in the application. We will take a look at exceptions and how to handle them in the next section.
Exceptions
We mentioned earlier that Python terminates the program upon encountering any errors while executing the statements. In this section, we will delve deep into errors and exceptions in Python.
We have faced many errors till now, such as NameError
, AttributeError
, IndexError
etc. Let's look into errors in Python in more detail.
Errors in Python are of at least two types:
- Syntax Errors
- Exceptions
Let's start with Syntax Errors.
Syntax Errors
Syntax errors, also called parsing errors, are the kind of errors where you deviate from the syntax that Python programming language understands. In the first chapter, we mentioned that programming languages are formal languages that adhere to a strict syntax. Python has a strict syntax and will complain when you write something; it doesn't allow or understand. For instance,
>>> print("Hello World)
File "<stdin>", line 1
print("Hello World)
^
SyntaxError: EOL while scanning string literal
In the code listing above, Python expects the string literal to have a matching double quote before the parenthesis. Python will repeat the offending line in syntax errors and put a little arrow mark where it encountered the error.
In the case of a script, Python prints the file name and line number to locate the error.
Syntax errors are easier to fix.
How can we fix the below code?
>>> a = {"name: "Joe", "age": 16"} # code to fix
File "<stdin>", line 1
a = {"name: "Joe", "age": 16"}
^
SyntaxError: invalid syntax
- Surround name with matching quotes
- Surround 16 with matching quotes
- Surround both name and 16 with matching quotes

Exceptions
Errors apart from the syntax errors, are called exceptions in Python.
Exceptions are errors caused by a syntactically correct expression or statement that python encounters while executing the program.
For instance, an undefined name will raise NameError
.
>>> print(my_name)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'my_name' is not defined
Another example is TypeError
when adding two incompatible types of objects.
>>> (1, 2, 3) + [4, 5, 6]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple
In the above code listing, Python encounters NameError
and TypeError
exceptions. Upon encountering exceptions, Python provides some additional information about the errors for you to fix them.
There are different types of exceptions in Python, and Python prints the type in the error message. Python assigns Standard exceptions to built-in identifiers. You can read about all the built-in exceptions in the python documentation[1].
Which of the following will raise a SyntaxError
?
print("Hello World)
'20' + 20
-0.5*3
(10 * (1/0)
When Python encounters an error inside a function, it returns a report containing function calls that led to a particular error. Because Python uses a stack to store function call, the report is called stack trace or stack traceback or simply traceback.
Stack Trace
Let's write another code example. In the code below, we define a function hello()
, which takes the required argument name
.
>>> def hello(name):
... print(f"Hi {name}")
If we call the hello()
function while not providing an argument, Python will raise an error.
>>> hello()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: hello() missing 1 required positional argument: 'name'
Since the first stack frame is the module frame, the traceback location is written in <module>
.
Now, let's take another example. We will define two additional functions and modify the definition of the hello()
function in the following manner.
def hello(name):
print(f"Hi {foo(name)}!") # calls `foo()` on the name
def foo(name):
return bar(name) # calls `bar()` on the name
def bar(name):
return name.upper() # returns the uppercased string
Now, let's test our function.
>>> hello("Luffy")
Hi LUFFY!
Great. It works.
hello()
function?
The upper()
method employed in the bar()
function definition is available only on string objects. If we pass int
objects to the hello()
function, Python will raise AttributeError
.
Now, let's pass an integer as an argument to the hello()
function.
>>> hello(12)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in hello
File "<stdin>", line 2, in foo
File "<stdin>", line 2, in bar
AttributeError: 'int' object has no attribute 'upper'
As we expected, Python raises AttributeError
.
In this case, you can notice the stack traceback printed in the error message. We can think of the call stack of the above function as something shown below.
If you compare the call stack diagram and the stack trace, you will find the resemblance—python prints out the call stack when it encounters an error.
In the stack traceback, Python lists out the call stack to narrow down the search while figuring out how and where the error occurred.
AttributeError: 'int' object has no attribute 'upper'
? What do you think is the utility of printing out the error message?
The error message AttributeError: 'int' object has no attribute, 'upper'
helps us understand what caused the error.
Now, let's look at some of the standard exceptions that you might come across, along with the reasons they get raised.
Common Exceptions and when to expect them
Let's look at some of the common exceptions that you might come across, along with the reasons they get raised.
Let's go through each one of them to understand them more.
NameError
The NameError
is raised when you have referenced an identifier, function, class, module, or other names that don't exist in your code. The Python documentation states
NameError
is raised when a local or global name is not found. Raised when a local or global name is not found.
Example:
>>> a = 1
>>> aaaaaaa # Causes `NameError` as not defined
IndexError
Python raises IndexError
when you attempt to retrieve an index from a sequence such as list
or tuple
, and the index isn’t present in the sequence. The Python documentation states:
Raised when a sequence subscript is out of range.
Example:
>>> a = (1, 2, 3)
>>> a[4] # Causes IndexError
ValueError
Python raises ValueError
when you provide an incorrect value of an object. It is more often encountered while you attempt to unpack more items from a sequence. Python documentation states:
Raised when an operation or function receives an argument with the right type but an inappropriate value. The situation is not described by a more specific exception such as IndexError.
Example:
>>> a, b, c = [1, 2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)
AttributeError
Python raises AttributeError
when you try to access an undefined or non-existent attribute on an object. The Python documentation states:
Raised when an attribute reference or assignment fails. (When an object does not support attribute references or attribute assignments, TypeError
is raised.)
Example:
>>> a = 1
>>> a.attr
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute 'attr'
ImportError
Python raises ImportError
when something goes wrong with an import statement.
Raised when theimport
statement has trouble trying to load a module. Also raised when the “from list” infrom ... import
has a name that cannot be found.
Example:
>>> from math import mat # Non-existent function import
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'mat' from 'math' (unknown location)
KeyError
Python raises KeyError
when you try to access a key that isn’t in a dictionary.
Raised when a mapping (dictionary) key is not found in the set of existing keys.
Example:
>>> person = {"name" : "Luffy"}
>>> person["age"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'age'
TypeError
Python raises a TypeError
exception whenever an operation is performed on an incorrect/unsupported object type.
Raised when an operation or function is applied to an object of inappropriate type. The associated value is a string giving details about the type mismatch.
Passing arguments of the wrong type (e.g., passing alist
when anint is expected) should result in a TypeError
, but passing arguments with the wrong value (e.g., a number outside expected boundaries) should result in aValueError
.
Some general cases where Python raises TypeError
:
- Unsupported operation between two types
>>> (1,2) + [3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple
- Calling a non-callable object
>>> name = "Luffy"
>>> name()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable
- Iterating through a non-iterable
>>> age = 16
>>> for x in age:
... print(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
The code below will encounter an error upon execution.
>>> a = lambda x: x + 1
>>> a((1,2))
TypeError
ValueError
NameError
IndexError
At some point, while designing programming, you are bound to encounter some form of exceptions. It is useful to learn how to handle those exceptions, which will do in the next section.
Handling Exceptions
In Python, there are two types of programming flavors regarding error-handling:
- Look before you leap ( LBYL )
- Easier to ask for forgiveness than permission ( EAFP )
Look before you leap
In the look before you leap ( LBYL ) style of handling errors, the programmer checks for exceptional cases or edge cases before executing a statement.
To illustrate the LBYL style, let's write a program that divides $100$ from each number from $-5$ to $5$ and prints out the result.
>>> for num in range(-5, 5):
... print("{:.1f}".format(100/num))
-20.0
-25.0
-33.3
-50.0
-100.0
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
In the above code listing, we can see that Python faces an error while dividing $100$ by $0$ and raises the exception ZeroDivisionError
. Now because we are aware that $0$ is present in the $range(-5, 5)$, we can add statements to ignore $0$ to avoid this exception altogether.
>>> for num in range(-5,5):
... if num == 0:
... continue # Ignore 0 and continue
... print("{:.1f}".format(100/num))
...
-20.0
-25.0
-33.3
-50.0
-100.0
100.0
50.0
33.3
25.0
In the above code listing, we avoided encountering errors as if prohibited division by $0$. The code listing demonstrates the look before your leap way of handling exceptions.
Because we are checking for potential errors beforehand, this error handling style is called Look before you leap.
Easier to ask for forgiveness
The other style of handling exception involves letting Python encounter errors and then dealing with them. This style of handling exceptions is called Easier to ask for forgiveness than permission( EAFP ).
We can write programs to handle selected exceptions using the try-except
clause. We can rewrite our previous program as follows :
>>> for num in range(-5, 5):
... try:
... print("{:.1f}".format(100/num))
... except ZeroDivisionError:
... print("Sorry, cannot divide by 0")
-20.0
-25.0
-33.3
-50.0
-100.0
Sorry, cannot divide by 0 # Asking for forgiveness
100.0
50.0
33.3
25.0
The way the try
statement works are as follows:
- First, Python executes the code block in the
try
clause - If no exception occurs, Python skips the code block in the
except
clause - If an exception occurs during the execution of a statement in the
try
clause code block, then the rest of the code block statements are skipped. - In case you write the name of the exception in the
except
clause, theexcept
clause catches the exception, and Python executes the code block in the clause. This is called handling the exception. - If an exception occurs, which does not match the exception named in the except clause, Python passes it to outer
try-except
statements. - If there are no handlers for the exception present, the exception becomes an unhandled exception, and the execution stops with a message.
In the EAFP style of error handling, we let Python encounter errors and handle the errors accordingly. Rather than preemptively checking if we can execute an operation ( asking for permission ), we let Python execute the operation and deal with the errors.
Python displays an error message upon encountering an error (asking for forgiveness ) and handles it appropriately. Let's see some more examples of using try-except
statements.
You should use the try-except
statements when you have reasons to believe that Python might encounter errors while executing. If you know the type of error that might arise, you can write the except clauses' errors. For instance,
try:
print(details) # 'details` not defined
except NameError as err:
print(f"Encountered NameError: {err}")
In the above script, we are aware that the name details
is not defined, and therefore it will raise NameError
. We can print the error message by writing NameError as err
in the except
clause. The following is the output of the above script.
The output of Error.py
is shown below:
>>> python3 Error.py
Encountered NameError: name 'details' is not defined
While taking inputs from the user, error handling can ensure that the user's input is correct. To take input from the user, we use the built-in function input()
.
while True:
try:
x = int(input("Enter a number: "))
break
except ValueError:
print("Sorry! Not a valid number. Please try again...")
The int()
function only accepts numeric string or numeric literals. It will raise ValueError
when you input other characters such as alphabets.
The output of UserInput.py
is shown below:
>>> python3 Error.py
Please enter a number: abc
Oops! That was no valid number. Try again...
Please enter a number: ^_^
Oops! That was no valid number. Try again...
Please enter a number: 1
UserInput.py
. What do you think will happen when the user types an incorrect input?
Every try
statement requires a corresponding except
clause. Therefore, Python will raise SyntaxError
and abruptly stop the program. Next, let's look at how to handle multiple exceptions in Python.
Multiple Exceptions Handling
A try-except
statement may have multiple except clauses to specify a handler for different exceptions. Python executes only one handler for any given exception.
You can pass various exceptions in the form of a tuple
to the except
clause or chain distinct except
clauses to handle separately. The last except
clause may omit the exception name(s) to catch any exception that occurs.
The last except
clause acts as a wildcard handler to catch any error that arises.
# Passing exceptions in tuple
try:
# Do Something
except(TypeError, NameError, ValueError):
# Handle Exceptions
# Separately handling exceptions
except TypeError:
# Do this
except NameError:
# Do this
except ValueError:
# Do this
except:
print("Unexpected Error: ", )
You can also choose not to do anything or silently pass after catching the error in the except
clause.
try:
# Do Something
except TypeError:
pass # Do nothing
However, it is a pretty bad idea to do nothing in the wildcard exception clause. The wildcard exception clause allows all sorts of errors to pass away, which becomes a headache when you are trying to debug in a large application silently. Don't do it, ever. Seriously.
try:
# Do something
except:
pass # VERY BAD IDEA. DON'T DO IT.
What's the output of the below script?
try:
a = [1, 2, 3] - [1, 2]
print(a)
except ValueError:
print("Something went wrong")
except NameError:
print("Something else went wrong")
except:
print("Something really went wrong")
- Something went wrong.
- Something else went wrong.
- Unknown thing went wrong.
[3]
The else
clause
Thetry-except
exception handling can also have anelse
clause that Python executes when it encounters no exceptions.
The else
clause must follow except clauses. We can see the general syntax of using the try-except-else
clause in the code listing below.
try:
# Do something
except Exception, e:
# Catch Errors
else:
# Execute this code if no exception is raised.
An instance of the try-except-else
clause is shown below.
# Block 1
try:
print("Trying to remove 3 from the list [1,2,3]")
a = [1,2,3] - [3] # Will result in TypeError
except TypeError:
print("Subtracting two lists directly is not supported")
else: # Will not be executed
print("Removed 3 from the list successfuly")
# Block 2
try:
print("Trying to remove 3 from the list [1,2,3]")
a = [1,2,3].remove(3) # Will be executed without exception
except:
print("Unexpected Error occured")
else: # Will be executed
print("Removed 3 from the list successfuly")
The output of TypeError.py
is shown below:
Trying to remove 3 from the list [1,2,3]
Subtracting two lists directly is not supported
Trying to remove 3 from the list [1,2,3]
Removed 3 from the list successfully
In the above code-listing :
- the first
try
clause tries to remove two lists, which causes theTypeError
Exception to be raised. As Python raises an exception from thetry
block, it doesn't execute theelse
clause for the first block. - The second
try
clause executes successfully without exceptions; therefore, Python executes theelse
block.
What's the output of the following script?
try:
a = [1, 2, 3, 4]
b = [1, 2, 3]
a.remove(b)
else:
print("Everything ran successfully")
except ValueError:
print("Something went wrong here")
except SyntaxError:
print("Wrong Syntax")
except:
print("Unexpected Error Occurred")
- Unexpected Error Occurred
- Wrong Syntax
- Something went wrong here
- Raises
SyntaxError
.
finally
clause
The try statement can take an additional finally
clause, which Python executes irrespective of whether it encounters an exception. Some things to note:
- If the
except
clause handles a raised exception, thefinally
clause executes after handling the exception. - If an exception is raised but not handled, the
finally
clause executes, and the exception is re-raised. - If Python encounters no exceptions,
finally
code block executes nonetheless.
Primarily, we use the finally clause to handle external resources such as files, network connections, or databases. We need to free these resources regardless of whether an operation is successful or not. These are cleanup operations after the execution of statements.
We can see the general syntax of using a try-except-else-finally
statement block in the following code listing.
try:
# Do Something
except Exception as e:
# Handle Exceptions
else:
# Executes if no exception is raised
finally:
# Executes always
Which of the following statements will not be printed after the script's execution?
try:
a = [1, 2, 3, 4]
print(a)
b = [1, 2, 3]
a.remove(b)
except ValueError:
print("Something went wrong here")
except:
print("Unexpected Error Occurred")
else:
print("Everything ran successfully")
finally:
print("Phew!")
- Something went wrong here
- Phew
- Everything ran successfully
[1, 2, 3, 4]
raising exceptions using raise
Earlier, we saw how useful these error messages have been. The exceptions raised by Python give clues and hints to what went wrong. The same is pretty valuable when you write your Python programs with informative exceptions.
When you raise information exceptions, your application users can quickly figure out what caused their errors. In Python, you can raise exceptions with the raise
statement. For instance,
>>> raise RuntimeError("Oops, something went wrong")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: Oops, something went wrong
To understand how this is useful, let's revisit the function that gives the factorial of a given number. We can write the factorial of a number as follows:
>>> def factorial(n):
... if n == 0:
... return 1
... return n*factorial(n-1)
Now, let's use the same function to get factorial numbers.
>>> factorial(5)
120
>>> factorial(7)
5040
>>> factorial(-2) # Error will be raised for negative numbers
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in factorial
File "<stdin>", line 4, in factorial
File "<stdin>", line 4, in factorial
[Previous line repeated 995 more times]
File "<stdin>", line 2, in factorial
RecursionError: maximum recursion depth exceeded in comparison
Our factorial()
works well for positive integers; however, it will throw an exception when someone uses a float
or a negative integer
. For someone who is using our function, the RecursionError
is not a useful message. Therefore, we will rewrite our factorial function to raise an error when a negative number or a floating-point number is given.
>>> def factorial(n):
... if n == 0:
... return 1
... if n < 0:
... raise ValueError("Number cannot be negative")
... if type(n) == float and not n.is_integer():
... raise ValueError("Number cannot be decimal")
... return n*factorial(n-1)
In our function factorial(), we made the following changes:
- It raises a
ValueError
exception when the argument number isnegative
. - It raises a
ValueError
exception when the argument number is anon-integer float
.
Raising these two errors will help the users to get more useful error messages.
For instance,
>>> factorial(2.3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in factorial
ValueError: Number cannot be decimal
>>> factorial(-2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in factorial
ValueError: Number cannot be negative
That's how we use the raise
statement to raise meaningful errors.
This brings us to the end of the chapter.
In the next chapter, we will start looking at some of the coolest things Python offers: Iterators, Generators, and Comprehensions.
Python documentation for built-in errors ↩︎
