print("condition True") if 1 > 0 else print("condition False")
>>> condition True
Control the flow of a computer program
When do you need to control the flow of a program?
if...[elif]....[else]
, match...case
for...[else]
, while...[else]
blockstry...[except]...[else]...[finally]
blocksNote: square brackets are generally used to represent something optional
As a beginner to programming conditional blocks and loops are sufficient to get started.
Error handlers are not needed for most of the basic stuff, but there are a few cases where it is useful to know the basics. For example it is useful to know the basics of using try
block for a few situations. Most of debugging is based on the concepts related to error handlers. Editors provide support for debugging using these concepts. Developers rely heavily on using these concepts to handle expected errors differently. Therefore it is useful to understand the basics of error handling as it is certain to make errors and handle them.
Asynchronous input/output (asyncio) and parallel computing are advanced topics used to increase efficiency of larger programs. They are not covered.
For all of the control flow techniques, conditionals, loops and error handlers
Conditional blocks use conditions to control and change the flow of a program. Conditions are created using boolean operations.
if...[elif]...[elif]...[else]
blocks
match...case...[case_]
blocks
if
blockif
blocksif
blocks are used to execute some code only when a condition is evaluated to True
.
Below are possible forms of if
block.
if condition:
# if block content
if condition:
# if block content
else:
# else block content
if condition_1:
# if block content
elif condition_2:
# elif block 1 content
elif condition_3:
# elif block 2 content
else:
# else block content
# execute some short expression conditionally
if condition: <short expression>
if
block
if
statement with an expression ending in colon (:
)if
block
elif
(else if) blockselse
blockelif
and else
is same as if
Python additionally provides a ternary form for short, one line conditionals. Below is the syntax, where X
and Y
are short expressions to be evaluated and returned.
Ternary operator is useful for code readability. It removes the requirement for a multiline if
block for quick checks required in code.
if condition else Y X
print("condition True") if 1 > 0 else print("condition False")
>>> condition True
print("condition True") if 1 == 0 else print("condition False")
>>> condition False
Flow diagram below illustrates the control flow for a if block.
Basic idea
if
and elif
conditions sequentially
True
False
else
block if present and exit= 12
some_num if some_num >= 0:
print(f'{some_num} is positive')
>>> 12 is positive
else
block= 12
some_num if some_num >= 0:
print(f'{some_num} is positive')
else:
print(f'{some_num} is negative')
>>> 12 is positive
elif
block= 12
some_num if some_num == 0:
print(f'{some_num} is zero')
elif some_num > 0:
print(f'{some_num} is positive')
elif some_num < 0:
print(f'{some_num} is negative')
>>> 12 is positive
elif
and else
= 12
some_num if some_num == 0:
print(f'{some_num} is zero')
elif some_num > 0:
print(f'{some_num} is positive')
else:
print(f'{some_num} is negative')
>>> 12 is positive
match...case
blockMatch blocks are used if some object is to be tested against multiple cases. It can be achieved using if
blocks but match
blocks are better for code readability and ease of use for the given use case.
_
is optional and for case when none of the options match.
match some_obj:
case option_1:
# do something and exit
case option_2:
# do something and exit
[case _:# do something and exit
]
if some_obj == option_1:
# do something and exit
elif some_obj == option_2:
# do something and exit
else
[# do something and exit
]
More information can be found at Python documentation for match statements.
for
blockwhile
blockIterating is a common term which refers to going through elements of a collection. Iterables and iterators in Python are based on this idea.
for
blockfor item in iterable:
# do something
# item is available
print(item)
for i in range(n):
# do something
# i is available
print(item)
for item in iterable: print(item)
Fundamental form
for
keyword declares the start of a for
blockitem in iterable
is the generic form:
, colon, to declare end of for
declarationUsing this fundamental form other variation are created. e.g.
range(n)
function is used to loop through fixed number of times
for i in range(10):
i takes values 0 through 9for i in range(1, 11):
i takes values 1 through 10range()
function is discussed at Section 11.7continue
continue
clause causes the loop to jump to next iteration without executing remaining lines in loop.
In the below structure, when the condition is true, item will not be printed.
for item in iterable:
# do something
# item is available
if condition:
continue
print(item)
Continue is useful if you need to skip execution of some code for certain elements of the iterable.
break
and else
break
clause causes exit of the innermost loop
else
clause
break
is hitfor item in iterable:
# do something
# item is available
if condition:
break
print(item)
else:
print("No break found")
n_max = 20
evens
odds
= 20
n_max = []; odds = []
evens for i in range(1, n_max + 1):
if i % 2 == 0 else odds.append(i) evens.append(i)
print(evens)
>>> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
print(odds)
>>> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
continue
In the below example only odd numbers are printed as the loop hits continue
for even numbers.
for i in range(5):
if i % 2 == 0: continue
print(i)
>>> 1
>>> 3
break
and else
= [65, 24, 53, 91, 59, 81, 93, 7, 78, 10]
correct_list for num in correct_list:
if num < 0 or num > 100:
print(f"validation failed, list contains {num}")
break
else:
print("validation successful")
>>> validation successful
= [97, 144, 115, 127, 33, 99, 85, 109, 21, 110]
incorrect_list for num in incorrect_list:
if 0 <= num <= 100:
continue
else:
print(f"validation failed, list contains {num}")
break
else:
print("validation successful")
>>> validation failed, list contains 144
Notice that
else
block is useful in this situation as it is run only when for loop iteration is complete without hittingbreak
statement.
There are different ways to structure the same conditions and required outcome.
Print table of numbers from 1 through 12 for multiplication with 1 through 10.
for i in range(1, 13):
for j in range(1, 11):
= "\n" if j == 10 else "\t"
end print(i*j, end=end)
>>> 1 2 3 4 5 6 7 8 9 10
>>> 2 4 6 8 10 12 14 16 18 20
>>> 3 6 9 12 15 18 21 24 27 30
>>> 4 8 12 16 20 24 28 32 36 40
>>> 5 10 15 20 25 30 35 40 45 50
>>> 6 12 18 24 30 36 42 48 54 60
>>> 7 14 21 28 35 42 49 56 63 70
>>> 8 16 24 32 40 48 56 64 72 80
>>> 9 18 27 36 45 54 63 72 81 90
>>> 10 20 30 40 50 60 70 80 90 100
>>> 11 22 33 44 55 66 77 88 99 110
>>> 12 24 36 48 60 72 84 96 108 120
while
while condition:
# code block
continue]
[# code block
break]
[else]:
[# code block
True
or break
statement is hitcontinue
, break
, else
clauses are optionalBelow diagram illustrates the control flow for a while loop.
while
block contents are executed repeatedly until condition is True
continue
statement is hit
break
statement is hit anywhere loop exits
else
block even if presentFalse
else
block is executed if presentA while loop is needed when number of repetitions is not known in advance
ctrl + c
to bail out= True
condition while condition:
# forget to set condition = False
print("inside infinite loop")
= 6
a while a != 0:
-= 1
a if a == 2:
print(f'break block: {a = }')
break
if a == 3:
print(f'continue block: {a = }')
continue
print(f'main block: {a = }')
>>> main block: a = 5
>>> main block: a = 4
>>> continue block: a = 3
>>> break block: a = 2
Below is gcd algorithm to find the greatest common divisor of 2 integers using while loop.
%
gives the remainder)Since the number of repetitions needed are based on input, while loop is suitable for this.
= 9, 6
a, b = a % b
rem while rem != 0:
print(f'{a = }, {b = }, {rem = }')
= b, rem
a, b = a % b
rem
print(f'{a = }, {b = }, {rem = }')
print(b)
>>> a = 9, b = 6, rem = 3
>>> a = 6, b = 3, rem = 0
>>> 3
Iterator is an object that can be iterated upon its elements only once. It is exhaustible.
Iterable is an object that can be iterated upon its elements repeatedly.
Both cannot be viewed directly and have to be converted into a list or tuple to view contents.
= "a", "b", "c", "d"
some_tuple = enumerate(some_tuple) some_iterator
print(list(some_iterator))
>>> [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]
print(list(some_iterator))
>>> []
Iterators | Iterables |
---|---|
zip |
range |
enumerate |
dictionary.keys |
open |
dictionary.values |
dictionary.items |
It is very common situation where both index and value is needed while iterating over a sequence (sequence is also an iterable).
Python provides a special function, enumerate(iterable)
to access index and value of items in an iterable.
= "a", "b", "c", "d"
some_tuple list(enumerate(some_tuple))
>>> [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]
for idx, item in enumerate(some_tuple):
print(f'{idx=}, {item=}')
>>> idx=0, item='a'
>>> idx=1, item='b'
>>> idx=2, item='c'
>>> idx=3, item='d'
It is possible to have access to index without enumerate
function, which is lengthier, inconvenient and less efficient, hence not required while using Python.
for idx in range(len(some_tuple)):
print(f'{idx=}, item={some_tuple[idx]}')
>>> idx=0, item=a
>>> idx=1, item=b
>>> idx=2, item=c
>>> idx=3, item=d
Python provides a zip(iterable1, iterable2, ...)
function which returns an iterator with tuples of elements of iterables provided until the shortest iterable is exhausted.
This is helpful when multiple iterables are needed to be iterated through in some connected way.
= [1, 2, 3]
some_list = (4, 5, 6)
some_tuple list(zip(some_list, some_tuple))
>>> [(1, 4), (2, 5), (3, 6)]
= []
result for e1, e2 in zip(some_list, some_tuple):
+ e2)
result.append(e1 print(result)
>>> [5, 7, 9]
This can be achieved without zip
but again since it is lengthier and inconvenient, it is not recommended while using Python.
= []
result for idx in range(len(some_list)):
+ some_list[idx])
result.append(some_tuple[idx] print(result)
>>> [5, 7, 9]
Python provides multiple functions to help iterating over dictionaries.
d.keys()
d.values()
d.items()
All of these return a view object which is an iterator. Most commonly used is d.items()
as it provides access to both key and value.
= {"key 1": "value 1", "key 2": "value 2", "key 3": "value 3"}
some_dict for k, v in some_dict.items():
print(f'key = {k}, value = {v}')
>>> key = key 1, value = value 1
>>> key = key 2, value = value 2
>>> key = key 3, value = value 3
It is important to remember that while iterating through mutable collections (list
, dict
, set
) it is recommended not to modify the collection as it gets complicated to handle unexpected situations. So best to use the solutions provided when in doubt rather than introducing a bug.
If the collection is immutable then modifying is not allowed and generally not used.
Another thing to note is modifying the collection in this context refers to changing the structure of collection, e.g. deleting an item. It does not refer to modifying the value of an item within the collection.
Solutions to this are, if a mutable collection is to be modified then
Below are some examples to illustrate when things go wrong when modifying a mutable collection while iterating through it.
In the list in example all numbers <= 3 were to be removed, but there is bug due to modifying the list while iterating over it.
Note that 2 is not removed. Figuring out in this case is simple. In the 2nd iteration 1 was already removed from the list and second item was 3 not 2.
This is a simple example hence finding the bug is easy, but as programs get larger it becomes difficult and inefficient to find such bugs.
= list(range(1, 6))
nums print(nums)
>>> [1, 2, 3, 4, 5]
for num in nums:
if num <= 3:
nums.remove(num)
print(nums)
>>> [2, 4, 5]
In case of dictionary it gives a run time error.
= {"k1": "v1", "k2": "v2", "k3": "v3"}
some_dict for k, v in some_dict.items():
if k == "k2":
del some_dict[k]
>>> Error: RuntimeError: dictionary changed size during iteration
List
= list(range(1, 6))
nums = []
new_nums for num in nums:
if not num <= 3:
new_nums.append(num)
print(nums)
>>> [1, 2, 3, 4, 5]
print(new_nums)
>>> [4, 5]
Dictionary
= {"k1": "v1", "k2": "v2", "k3": "v3"}
some_dict = {}
new_dict for k, v in some_dict.items():
if not k == "k2":
= v new_dict[k]
print(some_dict)
>>> {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}
print(new_dict)
>>> {'k1': 'v1', 'k3': 'v3'}
List
= list(range(1, 6))
nums print(nums)
>>> [1, 2, 3, 4, 5]
for num in nums.copy():
if num <= 3:
nums.remove(num)
print(nums)
>>> [4, 5]
Dictionary
= {"k1": "v1", "k2": "v2", "k3": "v3"}
some_dict print(some_dict)
>>> {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}
for k, v in some_dict.copy().items():
if k == "k2":
del some_dict[k]
print(some_dict)
>>> {'k1': 'v1', 'k3': 'v3'}
given the number of rules and syntax there are a lot of opportunities for errors
2 broad categories
Syntax error
=, 2 a
>>> invalid syntax (<string>, line 1)
# try:
# 1/0
# except Exception as e:
# print(e)
= 1/0 eg_num
>>> Error: ZeroDivisionError: division by zero
When a run time error occurs, program stops and exception messages are printed along with the traceback. Traceback is a track of where the error occurred in the program to which part of the program lead to running of the line which caused the error.
Python has some built-in known exceptions or error types. Exceptions are objects built using object oriented programming.
Exception
: base catch almost all errorsZeroDivisionError
: division by zeroTypeError
: operation not supported by type
[s] used in operationException
type object is created which has information regarding
Exception
typeBelow figure shows Python’s built-in Exception
hierarchy. More details can be found at Python documentation for built in exceptions.
raise
raise
statements causes the program to stop with default exception. Optionally a known error can be passed with a custom message, e.g. raise ValueError("Invalid input")
. raise
statement can be used anywhere in the program but is suited for use with try
and while
blocks, when an expected error is intercepted and has to handled differently.
raise ValueError("some custom message")
Below are some examples of common errors which are recommended to be tried and experimented in jupyter notebook cells. See the error messages and map them to the Exception hierarchy. Look at the traceback to see how it is structured.
1/0
>>> Error: ZeroDivisionError: division by zero
undefined_var_name
>>> Error: NameError: name 'undefined_var_name' is not defined
= [1, 2, 3]
some_list print(some_list[10])
>>> Error: IndexError: list index out of range
float("text")
>>> Error: ValueError: could not convert string to float: 'text'
"abc"*"xyz"
>>> Error: TypeError: can't multiply sequence by non-int of type 'str'
try
blockstry...except
blocks are used to intercept and handle expected run time errors differently. It provides many options like
Note that exception handling has a large number of specifications which can be combined in a lot of ways which can lead to complexity in the beginning. This is being covered with the objective of simple usage. All the specifications are for completeness and information.
try...[except]...[else]...[finally]
try:
# try code block
except Exception as e:
[# except code block
raise]]
[else:
[# else code block]
finally:
[# finally code block]
except
or finally
blockelse
clause (if present) should be after the except
blockexcept
clausesraise
statement and else
block are optionaltry
block statements are always evaluated firstexcept
blocks are run when they trap specified error
raise
statement is hit
finally
block is run<Exception type>
raisedelse
block is run in case of no errorfinally
block is run always, error or no error
except
blocksexcept
statement in a try
block is used to declare an except
block. They can be used in 2 forms.
except <Exception type>
except <Exception type> as <variable name>
except
block is run if there is a run time error while executing try
block and the error matches the <Exception type>
specified.
When second form is used, <variable name>
is bound to the <Exception type>
object.
There are 2 concepts involved.
Exception
is supposed to be handled, e.g. NameError
, TypeError
, etc.The first concept is of using the exception type. Base Exception
catches all known errors and is the most generic. In the beginning it should be enough. As the requirements increase there is a need to specify more specific errors. For multiple except
blocks, it is recommended to have specific exceptions before generic exceptions
The second is the error object. In the beginning it is not of much use as handling the exception should be enough, but as you progress it is this object which helps in dealing different errors differently as it holds a lot of information, like the traceback.
Try blocks are generally used to intercept expected errors and control the outcome if error occurs, use case depends on the context. e.g.
This is an example for nesting different control flow techniques.
The same problem serves as a use case for using recursive functions which is discussed in Section 13.9.1.3.
while True:
try:
= int(input('Enter an integer...'))
some_int print(some_int)
break
except ValueError:
continue