Basic Types#

Zen of Python#

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Numeric types: int, float, complex#

# int, float, complex
1, 1.0, 1j
(1, 1.0, 1j)
1j ** 2
(-1+0j)

Variables#

clicks = 2
clicks, type(clicks)
(2, int)
# type(object) == object's class
1
2
3  # cell outputs the value of the last expression
3
# variable binding

temperature = 10
temperature = 20
temperature = 36.6  # dynamic typing
temperature, type(temperature)
(36.6, float)
shows = 50
s4ows
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 s4ows

NameError: name 's4ows' is not defined
Anti-pattern: meaningless names of variables
a = 2
b = 10
c = b / a
c
5.0

int, float, arithmetic operations#

5 + 6 * (7 - 1) / 3
17.0
2**10
1024
# int / int = float

clicks = 12
shows = 10
ctr = clicks / shows

type(ctr), ctr
(float, 1.2)

Remark. CTR = Clicks-through rate is a popular metric: a ratio showing how often people who see your ad or free product listing end up clicking it.

Common mistake: Divisions / and // lead to different results
clicks // shows  # integer division
1
clicks % shows  # remainder 
2
value = 1
value += 1
value *= 2
value /= 2
value **= 3

value
8.0

Bit operations#

1 << 10, 8 >> 3
(1024, 1)
bin(8)
'0b1000'
0b100 ^ 0b010, 0b011 | 0b010, 0b011 & 0b010
(6, 3, 2)
bin(1023)
'0b1111111111'
Advice: bit operations are hard to read, and it's better to avoid them

Logical type: bool#

True, False
(True, False)
type(True), type(False)
(bool, bool)
not True, not False
(False, True)
True and False, True or False, (2 < 3)
(False, True, True)
Anti-pattern: omit brackets in ambiguous places
# Q: What's the result?
False == False != True
# How it works:
(False == False) and (False != True)
# A more obvious example:
2 < 3 < 5 < 7 < 11
(2 < 3) and (3 < 5) and (5 < 7) and (7 < 11)

String type: str#

greeting = "Hello"
goodbye = 'Bye-bye'

poem = """
В сто сорок солнц закат пылал, 
    в июль катилось лето, 
        была жара, 
            жара плыла - 
                на даче было это. 
Пригорок Пушкино горбил 
   Акуловой горою, 
       а низ горы - 
           деревней был, 
       кривился крыш корою. 
"""

type(greeting), type(goodbye), type(poem)   
(str, str, str)

Functions#

def square(x):
    return x*x
square(3.14)
9.8596
square(1j)
(-1+0j)
square('123')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[31], line 1
----> 1 square('123')

Cell In[28], line 2, in square(x)
      1 def square(x):
----> 2     return x*x

TypeError: can't multiply sequence by non-int of type 'str'
square(int("123"))
15129
from math import factorial
factorial(5)
120
factorial(20)
2432902008176640000
factorial(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
%%time
factorial(1000)
CPU times: user 61 µs, sys: 1 µs, total: 62 µs
Wall time: 68.9 µs
402387260077093773543702433923003985719374864210714632543799910429938512398629020592044208486969404800479988610197196058631666872994808558901323829669944590997424504087073759918823627727188732519779505950995276120874975462497043601418278094646496291056393887437886487337119181045825783647849977012476632889835955735432513185323958463075557409114262417474349347553428646576611667797396668820291207379143853719588249808126867838374559731746136085379534524221586593201928090878297308431392844403281231558611036976801357304216168747609675871348312025478589320767169132448426236131412508780208000261683151027341827977704784635868170164365024153691398281264810213092761244896359928705114964975419909342221566832572080821333186116811553615836546984046708975602900950537616475847728421889679646244945160765353408198901385442487984959953319101723355556602139450399736280750137837615307127761926849034352625200015888535147331611702103968175921510907788019393178114194545257223865541461062892187960223838971476088506276862967146674697562911234082439208160153780889893964518263243671616762179168909779911903754031274622289988005195444414282012187361745992642956581746628302955570299024324153181617210465832036786906117260158783520751516284225540265170483304226143974286933061690897968482590125458327168226458066526769958652682272807075781391858178889652208164348344825993266043367660176999612831860788386150279465955131156552036093988180612138558600301435694527224206344631797460594682573103790084024432438465657245014402821885252470935190620929023136493273497565513958720559654228749774011413346962715422845862377387538230483865688976461927383814900140767310446640259899490222221765904339901886018566526485061799702356193897017860040811889729918311021171229845901641921068884387121855646124960798722908519296819372388642614839657382291123125024186649353143970137428531926649875337218940694281434118520158014123344828015051399694290153483077644569099073152433278288269864602789864321139083506217095002597389863554277196742822248757586765752344220207573630569498825087968928162753848863396909959826280956121450994871701244516461260379029309120889086942028510640182154399457156805941872748998094254742173582401063677404595741785160829230135358081840096996372524230560855903700624271243416909004153690105933983835777939410970027753472000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
# A very inefficient recursive example, for education purposes only.
def my_factorial(n):
    if n == 0:
        return 1
    return n*my_factorial(n-1)
my_factorial(5)
120
my_factorial(10)
3628800
%%time
my_factorial(10000)
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
File <timed eval>:1

Cell In[37], line 5, in my_factorial(n)
      3 if n == 0:
      4     return 1
----> 5 return n*my_factorial(n-1)

Cell In[37], line 5, in my_factorial(n)
      3 if n == 0:
      4     return 1
----> 5 return n*my_factorial(n-1)

    [... skipping similar frames: my_factorial at line 5 (2967 times)]

Cell In[37], line 5, in my_factorial(n)
      3 if n == 0:
      4     return 1
----> 5 return n*my_factorial(n-1)

RecursionError: maximum recursion depth exceeded

Function print#

print(poem)
В сто сорок солнц закат пылал, 
    в июль катилось лето, 
        была жара, 
            жара плыла - 
                на даче было это. 
Пригорок Пушкино горбил 
   Акуловой горою, 
       а низ горы - 
           деревней был, 
       кривился крыш корою. 
print(65536)
print(10, 20, '123', True, False)
65536
10 20 123 True False
print('2', '+', 2)
print('2', '+', 2, sep='    ', end='!')
2 + 2
2    +    2!
# Q: What will happen?
print("Hello" + ", world!")
print('Two' * 5)
print("Two" + 2)
Hello, world!
TwoTwoTwoTwoTwo
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[48], line 4
      2 print("Hello" + ", world!")
      3 print('Two' * 5)
----> 4 print("Two" + 2)

TypeError: can only concatenate str (not "int") to str

Slices and substrings#

string = "Hello, world!"
character = string[10]  # NB: indexing starts from 0
sub_string = string[7:10]

type(string), type(character), type(sub_string)
(str, str, str)
character, sub_string
('l', 'wor')
'закат' in poem
True
string = "Hello, world!"
print(string[-1])
print(string[0])
!
H
print(string[:5])
print(string[7:])
print(string[4:8])
print(string[-5:-1])
Hello
world!
o, w
orld
string[:]
'Hello, world!'
print(string[::2])  # every second symbol
print(string[::-1])  # reversed string
Hlo ol!
!dlrow ,olleH

String literals#

(
    'Hello'
    ', world'
    '!'
)
'Hello, world!'
'123' + \
'456' + \
'789'
'123456789'
'123' \
'456' \
'789'
'123456789'

Dict#

birthdays = {'Alice': 123, 'Bob': '1999-12-31'}
birthdays['Sam'] = '1972-01-01'
birthdays['Sam']
'1972-01-01'
birthdays
{'Alice': 123, 'Bob': '1999-12-31', 'Sam': '1972-01-01'}
birthdays.get(123) is None
True
birthdays.get('Alice')
123
del birthdays['Bob']
birthdays
{'Alice': 123, 'Sam': '1972-01-01'}
'Alice' in birthdays
True
'Bob' in birthdays
False
empty_dict = dict()
len(empty_dict)
0
empty_dict
{}
len(birthdays)
2

List#

math_names = ['sin', 'cos', 'rot', 'div']
type(math_names)
list
len(math_names)
4
'cos' in math_names, 'sos' in math_names
(True, False)
math_names[0], math_names[1], math_names[-1]
('sin', 'cos', 'div')
math_names[1:3]
['cos', 'rot']
math_names[::-1]
['div', 'rot', 'cos', 'sin']
# Q: What will happen?
print([1, 2] + [3, 4])
print([2, 3] * 4)
print([2, 3] + 4)
[1, 2, 3, 4]
[2, 3, 2, 3, 2, 3, 2, 3]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[75], line 4
      2 print([1, 2] + [3, 4])
      3 print([2, 3] * 4)
----> 4 print([2, 3] + 4)

TypeError: can only concatenate list (not "int") to list
letters = ['alpha', 'beta', 'gamma']
letters.append('delta')

letters
['alpha', 'beta', 'gamma', 'delta']
books = ['Философский камень', 'Тайная комната']
books += ['Узник Азкабана']
books
['Философский камень', 'Тайная комната', 'Узник Азкабана']
letters.pop(), letters  # pop deletes the last element of the list and returns it
('delta', ['alpha', 'beta', 'gamma'])
letters.pop(1), letters  # delete element at position 1
('beta', ['alpha', 'gamma'])
Advice: use list as a mutable container of values of similar types
Anti-pattern: mix values of different type in a list
yyyyy = [555, "World", [1,2,3], True, 10.5]
Anti-pattern: self-referencing
inception = ['dream']
inception.append(inception)
inception
['dream', [...]]
inception[1][1][1][1][1][1][1][1][1][1][1][1]
['dream', [...]]
type(inception)
list

Tuple#

(1, 2, 3)
(1, 2, 3)
(True, 100)
(True, 100)
(complex, 21, False)
(complex, 21, False)
21 in (complex, 21, False)
True
print((1, 2) + (3, 4))
print((True, False) * 10)
(1, 2, 3, 4)
(True, False, True, False, True, False, True, False, True, False, True, False, True, False, True, False, True, False, True, False)
print((20, 30) + 40)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[90], line 1
----> 1 print((20, 30) + 40)

TypeError: can only concatenate tuple (not "int") to tuple
ls = [1,2, 3]
ls
[1, 2, 3]
ls[1] = 10
ls
[1, 10, 3]
t = (4, 5, 6)
t
(4, 5, 6)
t[1] = 20
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[94], line 1
----> 1 t[1] = 20

TypeError: 'tuple' object does not support item assignment

Set#

{'foo', 'bar', 'foobar'}
{'bar', 'foo', 'foobar'}
{'foo', 'bar', 'foobar', 'foo', 'foo'}
{'bar', 'foo', 'foobar'}
s = {1, 2, 3}
s[1]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[97], line 2
      1 s = {1, 2, 3}
----> 2 s[1]

TypeError: 'set' object is not subscriptable
set("A man, a plan, a canal, - Panama")
{' ', ',', '-', 'A', 'P', 'a', 'c', 'l', 'm', 'n', 'p'}
nile ="""
    1120 1160 963 1210 1160 1160 813 1230 1370 1140 995 935 1110
    994 1020 960 1180 799 958 1140 1100 1210 1150 1250 1260 1220
    1030 1100 774 840 874 694 940 833 701 916 692 1020 1050 969
    831 726 456 824 702 1120 1100 832 764 821 768 845 864 862 698
    845 744 796 1040 759 781 865 845 944 984 897 822 1010 771 676
    649 846 812 742 801 1040 860 874 848 890 744 749 838 1050 918
    986 797 923 975 815 1020 906 901 1170 912 746 919 718 714 740
"""
nile_list = nile.split()
print(nile_list[:10])
print("Size of list:", len(nile_list))
['1120', '1160', '963', '1210', '1160', '1160', '813', '1230', '1370', '1140']
Size of list: 100
#Q. How to get all unique values from the list?
s = set(nile_list)
print(type(s))
l = list(s)
print(type(l))
len(list(set(nile_list)))
<class 'set'>
<class 'list'>
85
print(s)
{'860', '846', '1370', '1210', '781', '1170', '801', '963', '742', '821', '897', '1100', '813', '822', '984', '698', '1050', '718', '759', '848', '824', '901', '940', '1260', '1020', '1250', '1220', '995', '1030', '916', '994', '694', '768', '1230', '771', '906', '1040', '1150', '1120', '831', '456', '740', '714', '749', '865', '701', '774', '890', '726', '815', '862', '1140', '692', '874', '944', '912', '840', '918', '1180', '864', '975', '764', '812', '833', '676', '796', '746', '1010', '1160', '1110', '838', '960', '923', '649', '744', '845', '958', '969', '799', '919', '702', '797', '935', '832', '986'}
print(list(s)[:10])
['860', '846', '1370', '1210', '781', '1170', '801', '963', '742', '821']

Packing, unpacking#

int, 21, False
(int, 21, False)
type(_)  # the variable _ stores the result of the last expression evaluation
tuple
numbers = (4, 8, 15, 16, 23, 42)
a, b, c, d, e, f = numbers
a
4
e
23
colors = ('red', 'green', 'blue')
red_variable, *other_colors = colors
red_variable
'red'
other_colors
['green', 'blue']
letters = 'abcdefgh'
*other, prev, last = letters
print(other, prev, last)
['a', 'b', 'c', 'd', 'e', 'f'] g h
# pack, transfer, unpack

type_of_object = float
text = '-273.15'

pack = (type_of_object, text)
# ...
call, value = pack
call(value)
# swap a and b
a = 10
b = 20
print(a, b)

a, b = b, a
print(a, b)

Implicit type conversion#

True + 1
2
1 + 1.0
2.0
True + 1.0
2.0
True == 1, \
1.0 == 1, \
"1" == 1
(True, True, False)

Explicit type conversion#

int(4.9), int("345"), float(-7), float("1.4")
(4, 345, -7.0, 1.4)
float("-inf"), float("inf"), float("nan")
(-inf, inf, nan)
list("abc")
['a', 'b', 'c']
str([1,2,3])
'[1, 2, 3]'
tuple(["Obi", "Wan", "Kenobi"])
('Obi', 'Wan', 'Kenobi')
bool(-2), bool(-1), bool(0), bool(1), bool(2)
(True, True, False, True, True)
bool(-100.0), bool(0.0), bool(1000.0)
bool(-1j), bool(0j+0), bool(3j)
bool(""), bool("True"), bool("False"), bool("[]")
bool([]), bool([10, 20, 30]), bool([True]), bool([False])
bool(()), bool((False)), bool((False,))

Control flow#

Condition operator, if, elif, else#

x = 35
if x % 10 == 0:  # semicolon emphasizes the beginning of еру new code block
    
    # indentation is important!

    print(x, "Divisible by 10")
elif x % 5 == 0:
    print(x, "Divisible by 5")
else:
    print(x, "Not divisible by 5")
35 Divisible by 5
value = 4

if value > 5:
    print('> 5')
elif value == 5:
    print('5')
elif value < 5:
    print('< 5')
else:
    print('HOW?!')  # Q: for which value "HOW?!" will be printed?
< 5
Anti-pattern: redundant brackets
if (x % 10 == 0):  # unnecessary brackets impede reading of this code
    print(x, "Divisible by 10")
elif (x % 5 == 0):
    print(x, "Divisible by 5")
else:
    print(x, "Not divisible by 5")
35 Divisible by 5

Ternary operator#

if x % 10 == 0:
    answer = "Divisible by 10"
else:
    answer = "Not divisible by 10"
answer = "Divisible by 10" if x % 10 == 0 else "Not divisible by 10"
Anti-pattern: use a ternary operator for large expressions
../../_images/big_ternary.png
Anti-pattern: return different types depending on the condition
x = 13

answer = x % 10 if x > 0 else "Wrong value!"

Execsice -1. Write a function, which returns the middle of three given integers.

def get_middle_value(a: int, b: int, c: int) -> int:
    """
    Takes three values and returns middle value.
    """
    pass
assert get_middle_value(1, 2, 3) == 2
assert get_middle_value(1, 3, 2) == 2
assert get_middle_value(2, 1, 3) == 2
assert get_middle_value(2, 3, 1) == 2
assert get_middle_value(3, 1, 2) == 2
assert get_middle_value(3, 2, 1) == 2
assert get_middle_value(-100, -10, 100) == -10
assert get_middle_value(100, -100, -10) == -10
assert get_middle_value(-10, -10, -5) == -10
assert get_middle_value(-10, -10, -10) == -10
assert get_middle_value(-100, 10, 100) == 10
assert get_middle_value(0, 0, 0) == 0
assert get_middle_value(10**12, -10**12, 10**10) == 10**10

or, and#

bool(20), bool(0)
(True, False)
bool(20 or 0), bool(20 and 0)
(True, False)
type(20 or 0), type(20 and 0)
(int, int)
x = ""
if x:
    pass  # pass just does nothing
else:
    x = "default"
    
x
'default'
x = ""
x = x or "default"  # often pattern to substitute empty value

x

Lazy evaluation#

#Q: what will happen?
print(0 and NEVER_EXISTED_VARIABLE)
print([10] or NEVER_EXISTED_VARIABLE)
0
[10]
print(10 and NEVER_EXISTED_VARIABLE)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[152], line 1
----> 1 print(10 and NEVER_EXISTED_VARIABLE)

NameError: name 'NEVER_EXISTED_VARIABLE' is not defined
10 or print("lazy")
10
print("start") or print("end")
start
end
print("start")

None#

# None means the value is missing
None
type(_)  # sic! None is not stored to _ (because "value is missing")
         # so _ contains one of previous values
int
type(None)
NoneType
bool(None), str(None)
(False, 'None')

None usage#

# Turning video on...

is_working = False  # something is broken

if is_working:
    channel = 'Megasuper Channel'
else:
    channel = None 
value = print("I am print!")  # print just prints objects, and returns None
    
print("value", "=", value)
I am print!
value = None
Аnti-pattern: checking `== None` instead of `is None`
# print size of value, if value is not None

value = None
# value = "Football"
# value = ""

# Newbie
if value != None:  # works for basic types, but 
                   # nonstandard objects could equal None,
                   # not beeing None
    print(len(value))

# Newbie 2
if value:  # empty list coerced to False 
    print(len(value))
    
# Expert
if value is not None:  # is checks if value is the same object
    print(len(value))

Loops: for#

# print numbers from 1 to 10
print(1)
print(2)
print(3)
print(4)
print(5)
print(6)
print(7)
print(8)
print(9)
print(10)
1
2
3
4
5
6
7
8
9
10
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
    print(n)
1
2
3
4
5
6
7
8
9
10
for letter in "ABCDE":
    print("Letter", letter)
Letter A
Letter B
Letter C
Letter D
Letter E
for obj in (int, 10.1, True, None):
    print(obj)
<class 'int'>
10.1
True
None
for k in {"a": 1, "b": 2}:
    print(k)
a
b
d = {"a": 1, "b": 2}
for k, v in d.items():
    print(k ,v)
a 1
b 2
# print numbers 1 to 1000000

for n in [1, 2, 3, ]:  # too long to type...
    print(n)

range#

numbers = range(10)
type(numbers)
range
numbers[0], numbers[-1]
(0, 9)
reversed_numbers = numbers[::-1]
reversed_numbers
list(range(10))
list(range(4, 14))  # half-open interval [4, 14)
list(range(4, 14, 3))
list(range(14, 4, -2))
range(10**100)  # does not fit in memory, but range can handle it
range('a', 'f', 2)  # Q: will it work?

Loops: for#

# Print numbers from 0 to 1000000

for n in range(1000000):
    # print(n)  # закомментировано, чтоб случайно не напечатать и не подвесить jupyter ;]
    pass
# print alphabet in direct and reversed orders
alphabet = 'abcdefghijklmnopqrstuvwxyz'
for letter in alphabet:
    print(letter, end=' ')
    
print()
    
for letter in alphabet[::-1]:
    print(letter, end=' ')
a b c d e f g h i j k l m n o p q r s t u v w x y z 
z y x w v u t s r q p o n m l k j i h g f e d c b a 
Anti-pattern: [::-1] is read poorly. It's better to use reversed
reversed(alphabet)
<reversed at 0x10e1cda20>
for letter in reversed(alphabet):
    print(letter, end=' ')
z y x w v u t s r q p o n m l k j i h g f e d c b a 
for value in range(9, -1, -1):
    print(value, end=' ')

print()

for value in reversed(range(10)):
    print(value, end=' ')
9 8 7 6 5 4 3 2 1 0 
9 8 7 6 5 4 3 2 1 0 

Exersice 0. Write five versions of reversing list program.

def reverse_iterative(lst: list[int]) -> list[int]:
    """
    Return reversed list. You can use only iteration
    :param lst: input list
    :return: reversed list
    """
    # lst = [1, 5, 7, 8, -54] -> [-54, 8, 7, 5, 1]
    result = list()
    for i in range(len(lst)):
        result.append(lst[len(lst) - i - 1])
    return result

def reverse_inplace_iterative(lst: list[int]) -> None:
    """
    Revert list inplace. You can use only iteration
    :param lst: input list
    :return: None
    """
    # YOUR CODE HERE


def reverse_inplace(lst: list[int]) -> None:
    """
    Revert list inplace with reverse method
    :param lst: input list
    :return: None
    """
    # YOUR CODE HERE


def reverse_reversed(lst: list[int]) -> list[int]:
    """
    Revert list with `reversed`
    :param lst: input list
    :return: reversed list
    """
    # YOUR CODE HERE

def reverse_slice(lst: list[int]) -> list[int]:
    """
    Revert list with slicing
    :param lst: input list
    :return: reversed list
    """
    # YOUR CODE HERE
reverse_iterative([1, 5, 7, 8, -54])
reverse_slice([1, 5, 7, 8, -54])

Excercise 1. The legendary FizzBuzz (if you think that it is too easy, look here).

def FizzBuzz(n):
    """
    Prints numbers from 1 to n.
    But for the multiples of three print 'Fizz' instead of the number 
    and for the multiples of five print 'Buzz'.
    For numbers which are multiples of both three and five print 'FizzBuzz'.
    """
    # YOUR CODE HERE
FizzBuzz(5)
FizzBuzz(15)

Loops: while#

data = [.0, .0, .0, 1.2]

while len(data) > data[-1]:
    last = data[-1]
    data.append(last * last)
    
    print(data)
[0.0, 0.0, 0.0, 1.2, 1.44]
[0.0, 0.0, 0.0, 1.2, 1.44, 2.0736]
[0.0, 0.0, 0.0, 1.2, 1.44, 2.0736, 4.299816959999999]
[0.0, 0.0, 0.0, 1.2, 1.44, 2.0736, 4.299816959999999, 18.488425889503635]
def gcd(a, b):
    # Euclid's algorithm
    while a > 0:
        if a > b:
            a, b = b, a
        a, b = b % a, a
    return b

gcd(10010, 1100)
110
Advice: use for when elements are handled sequentially and independently
data = [10, 20, 30, 40]

print("Print every element in the list")

for value in data:
    print(value, end=', ')

print()
print("For every element in the list print: (index, element)")

for i, value in enumerate(data):
    # enumerate maps data objects to the tuple (index, object)
    print(i, value, end=', ')

print()
print("Print index every element in the list")

for i in range(len(data)):
    print(i, end=', ')
Anti-pattern: alter the object being iterated by in for loop
data = [0,1,2,3,4,5,6,7,8,9,10]

for value in data:
    print(value, end=', ')
    data.pop(0)  # delete the first element
Рекомендация: use while if the list changes at each iteration, or if the number of iterations is unpredictible
data = [0,1,2,3,4,5,6,7,8,9,10]

while data:
    value = data.pop(0)
    print(value, end=', ')

Excercise 2. Collatz conjecture states that for any \(n \in \mathbb N\) the iterative calculation of the function $\( f(n) = \begin{cases} \frac n2,& n \text{ is even,}\\ 3n+1, & n \text{ is odd.} \end{cases} \)\( finally reaches \)1$. For example:

  • \(f(2) = 1\) (one iteration);

  • \(f(3) = 10\), \(f(10) = 5\), \(f(5) = 16\), \(f(16) = 8\), \(f(8) = 4\), \(f(4) = 2\), \(f(2) = 1\) (seven iterations);

  • \(f(4) = 2\), \(f(2) = 1\) (two iterations).

Calculate the number of steps for n to reach 1.

def collatz_steps(n):
    # YOUR CODE HERE
# run this cell to test your solution

ground_truth = {1: 0,
                2: 1,
                3: 7,
                4: 2,
                5: 5,
                6: 8,
                7: 16,
                8: 3,
                9: 19,
                27: 111,
                257: 122,
                1000: 111,
                100500: 66
               }

for n, steps in ground_truth.items():
    got_steps = collatz_steps(n)
    assert got_steps == steps, f"Expected {steps}, got: {got_steps}"

Loops: break, continue, else#

contnue#

# continue stops the execution of the current iteration and goes to the next one
skip = 7

for n in range(10):
    if n == skip:
        print('*', end=', ')
        continue
    print(n, end=', ')
data = [1, 2, 3, 4, 5, 6, 7]

while data:
    value = data.pop()  # remove the last element
    if 2 <= len(data) <= 4:
        continue
    print(value, end=', ')

break#

# break interrupts the loop execution
# break interrupts the loop execution

for natural in [1, 2, 3, 4, -1, 20]:
    if natural < 0:
        print("ERROR")
        break
    
ERROR
from datetime import datetime

secret_number = 1 + datetime.now().microsecond % 100

while True:
    n = int(input("Enter your guess:"))
    if n == secret_number:
        print("You win!")
        break
    if n == 0:
        print("The secret number was", secret_number)
        break

else#

# if the loop was not finished by break then else-block executes
for line in ['Hello', 'World', 'Library']:
    if len(line) > 7:
        print("Found long line!")
        break
else:
    print("All lines are short")

Built-ins#

dir(__builtins__)
['ArithmeticError',
 'AssertionError',
 'AttributeError',
 'BaseException',
 'BaseExceptionGroup',
 'BlockingIOError',
 'BrokenPipeError',
 'BufferError',
 'BytesWarning',
 'ChildProcessError',
 'ConnectionAbortedError',
 'ConnectionError',
 'ConnectionRefusedError',
 'ConnectionResetError',
 'DeprecationWarning',
 'EOFError',
 'Ellipsis',
 'EncodingWarning',
 'EnvironmentError',
 'Exception',
 'ExceptionGroup',
 'False',
 'FileExistsError',
 'FileNotFoundError',
 'FloatingPointError',
 'FutureWarning',
 'GeneratorExit',
 'IOError',
 'ImportError',
 'ImportWarning',
 'IndentationError',
 'IndexError',
 'InterruptedError',
 'IsADirectoryError',
 'KeyError',
 'KeyboardInterrupt',
 'LookupError',
 'MemoryError',
 'ModuleNotFoundError',
 'NameError',
 'None',
 'NotADirectoryError',
 'NotImplemented',
 'NotImplementedError',
 'OSError',
 'OverflowError',
 'PendingDeprecationWarning',
 'PermissionError',
 'ProcessLookupError',
 'RecursionError',
 'ReferenceError',
 'ResourceWarning',
 'RuntimeError',
 'RuntimeWarning',
 'StopAsyncIteration',
 'StopIteration',
 'SyntaxError',
 'SyntaxWarning',
 'SystemError',
 'SystemExit',
 'TabError',
 'TimeoutError',
 'True',
 'TypeError',
 'UnboundLocalError',
 'UnicodeDecodeError',
 'UnicodeEncodeError',
 'UnicodeError',
 'UnicodeTranslateError',
 'UnicodeWarning',
 'UserWarning',
 'ValueError',
 'Warning',
 'ZeroDivisionError',
 '__IPYTHON__',
 '__build_class__',
 '__debug__',
 '__doc__',
 '__import__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'abs',
 'aiter',
 'all',
 'anext',
 'any',
 'ascii',
 'bin',
 'bool',
 'breakpoint',
 'bytearray',
 'bytes',
 'callable',
 'chr',
 'classmethod',
 'compile',
 'complex',
 'copyright',
 'credits',
 'delattr',
 'dict',
 'dir',
 'display',
 'divmod',
 'enumerate',
 'eval',
 'exec',
 'execfile',
 'filter',
 'float',
 'format',
 'frozenset',
 'get_ipython',
 'getattr',
 'globals',
 'hasattr',
 'hash',
 'help',
 'hex',
 'id',
 'input',
 'int',
 'isinstance',
 'issubclass',
 'iter',
 'len',
 'license',
 'list',
 'locals',
 'map',
 'max',
 'memoryview',
 'min',
 'next',
 'object',
 'oct',
 'open',
 'ord',
 'pow',
 'print',
 'property',
 'range',
 'repr',
 'reversed',
 'round',
 'runfile',
 'set',
 'setattr',
 'slice',
 'sorted',
 'staticmethod',
 'str',
 'sum',
 'super',
 'tuple',
 'type',
 'vars',
 'zip']
Anti-pattern: use builtins as names for variables
# Q: What will happen?
str = 'hello'
str(1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[179], line 3
      1 # Q: What will happen?
      2 str = 'hello'
----> 3 str(1)

TypeError: 'str' object is not callable
str = __builtins__.str  # Recovering
print, reversed, enumerate
# built-in functions
min, max, sum, ...
Anti-pattern: do not use built-ins for simple operations
data = [1, 2, 3, 4, 5]

# Newbie
total = 0
for el in data:
    total += el
    
# Expert
total = sum(data)

total
data = [1, 2, 3, 4, 5]

# Newbie

min_value = None

for el in data:
    if min_value is None:
        min_value = el
    else:
        if el < min_value:
            min_value = el
    
# Expert
min_value = min(data)
# Q: What's the value of this expression?
max([])
# newbie
for value in [1, 2, 3, 4, 5]:
    print(value, end=', ')

print()

# expert
for value in range(1, 6):
    print(value, end=', ')
data = [1, 2, 3, 4, 5]

# newbie
for i in range(len(data)):
    print(i, data[i], end=', ')

print()
    
# expert
for i, value in enumerate(data):
    print(i, value, end=', ')
data = [1, 2, 3, 4, 5]
letters = ['a', 'b', 'c', 'd', 'e']

# newbie
for i in range(len(data)):
    print(i, (data[i], letters[i]), end=', ')

print()
    
# expert
for i, value in enumerate(zip(data, letters)):
    print(i, value, end=', ')
math_vector_first = [1.0, -3.5, 4.1]
math_vector_second = [7.0, -1.1, -1.4]
#math_vector_sum = ? Using numpy is a better choice!

math_vector_sum = []
for element_first, element_second in zip(math_vector_first, math_vector_second):
    # теперь на каждой итерации мы видим пару элементов и можем их сложить
    math_vector_sum.append(
        element_first + element_second
    )
    
math_vector_sum

Sorting#

lst = [-1, 5, 7.34, 0.15, 2.5, -3]
sorted(lst)
[-3, -1, 0.15, 2.5, 5, 7.34]
sorted(lst, reverse=True)
[7.34, 5, 2.5, 0.15, -1, -3]
# make list of ints from nile.txt file, using list comprehension
nile_lst = [int(s) for s in nile.split()]

print(sorted(nile_lst))

Exercise 3. The merging part of Merge Sort.

def merge_iterative(lst_a: list[int], lst_b: list[int]) -> list[int]:
    """
    Merge two sorted lists in one sorted list
    :param lst_a: first sorted list
    :param lst_b: second sorted list
    :return: merged sorted list
    """
    # YOUR CODE HERE
    

def merge_sorted(lst_a: list[int], lst_b: list[int]) -> list[int]:
    """
    Merge two sorted lists in one sorted list using `sorted`
    :param lst_a: first sorted list
    :param lst_b: second sorted list
    :return: merged sorted list
    """
    # YOUR CODE HERE
assert merge_sorted([],[]) == []
assert merge_iterative([],[]) == []
assert merge_sorted([1],[]) == [1]
assert merge_iterative([1],[]) == [1]
assert merge_sorted([],[1]) == [1]
assert merge_iterative([],[1]) == [1]
assert merge_sorted([1],[1]) == [1, 1]
assert merge_iterative([1],[1]) == [1, 1]
assert merge_sorted([2],[1]) == [1, 2]
assert merge_iterative([2],[1]) == [1, 2]
assert merge_sorted([1],[2]) == [1, 2]
assert merge_iterative([1],[2]) == [1, 2]
assert merge_sorted([1, 3],[2, 4]) == [1, 2, 3, 4]
assert merge_iterative([1, 3],[2, 4]) == [1, 2, 3, 4]
assert merge_sorted([1, 3, 3, 4],[2, 3,  4]) == [1, 2, 3, 3, 3, 4, 4]
assert merge_iterative([1, 3, 3, 4],[2, 3, 4]) == [1, 2, 3, 3, 3, 4, 4]

Exercise 4. Using binary search, determine wether a value is present in a sorted list.

def find_value(l: list[int], v: int):
    """
        Returns True iff the value v is present in the increasingly sorted list l.
    """
    # YOUR CODE HERE    
# Run this and the next cell to test your solution
import copy
import dataclasses

@dataclasses.dataclass
class Case:
    nums: list[int] | range
    value: int
    result: bool
    name: str | None = None

    def __str__(self) -> str:
        if self.name is not None:
            return self.name
        return 'find_{}_in_{}'.format(self.value, self.nums)

BIG_VALUE = 10**15

TEST_CASES = [
    Case(nums=[], value=2, result=False),
    Case(nums=[1], value=2, result=False),
    Case(nums=[1, 3, 5], value=0, result=False),
    Case(nums=[1, 3, 5], value=2, result=False),
    Case(nums=[1, 3, 5], value=4, result=False),
    Case(nums=[1, 3, 5], value=6, result=False),
    Case(nums=[1, 3, 5], value=1, result=True),
    Case(nums=[1, 3, 5], value=3, result=True),
    Case(nums=[1, 3, 5], value=5, result=True),
    Case(nums=[3], value=3, result=True),
    Case(nums=[1, 3], value=1, result=True),
    Case(nums=[1, 3], value=3, result=True),
    Case(nums=[1, 3, 5, 7], value=0, result=False),
    Case(nums=[1, 3, 5, 7], value=2, result=False),
    Case(nums=[1, 3, 5, 7], value=4, result=False),
    Case(nums=[1, 3, 5, 7], value=6, result=False),
    Case(nums=[1, 3, 5, 7], value=8, result=False),
    Case(nums=[1, 3, 5, 7], value=1, result=True),
    Case(nums=[1, 3, 5, 7], value=3, result=True),
    Case(nums=[1, 3, 5, 7], value=5, result=True),
    Case(nums=[1, 3, 5, 7], value=7, result=True),
    Case(nums=[1, 3, 5, 7, 9], value=0, result=False),
    Case(nums=[1, 3, 5, 7, 9], value=2, result=False),
    Case(nums=[1, 3, 5, 7, 9], value=4, result=False),
    Case(nums=[1, 3, 5, 7, 9], value=6, result=False),
    Case(nums=[1, 3, 5, 7, 9], value=8, result=False),
    Case(nums=[1, 3, 5, 7, 9], value=10, result=False),
    Case(nums=[1, 3, 5, 7, 9], value=1, result=True),
    Case(nums=[1, 3, 5, 7, 9], value=3, result=True),
    Case(nums=[1, 3, 5, 7, 9], value=5, result=True),
    Case(nums=[1, 3, 5, 7, 9], value=7, result=True),
    Case(nums=[1, 3, 5, 7, 9], value=9, result=True),
    Case(nums=[1, 5, 5, 5, 9], value=1, result=True),
    Case(nums=[1, 5, 5, 5, 9], value=5, result=True),
    Case(nums=[1, 5, 5, 5, 9], value=9, result=True),
    Case(nums=[1, 5, 5, 5, 9], value=7, result=False),
    Case(nums=range(0, BIG_VALUE, 2), value=BIG_VALUE - 2, result=True, name="max_in_big_range"),
    Case(nums=range(0, BIG_VALUE, 2), value=0, result=True, name="min_in_big_range"),
    Case(nums=range(0, BIG_VALUE, 2), value=BIG_VALUE, result=False, name="greater_than_max_in_big_range"),
    Case(nums=range(0, BIG_VALUE, 2), value=-1, result=False, name="less_than_min_in_big_range"),
    Case(nums=range(0, BIG_VALUE, 2), value=BIG_VALUE // 2, result=True, name="middle_in_big_range"),
    Case(nums=range(0, BIG_VALUE, 2), value=BIG_VALUE // 2 + 1, result=False, name="middle_not_exists_in_big_range"),
]

def test_find_value(t: Case) -> None:
    nums_copy = copy.deepcopy(t.nums)

    answer = find_value(nums_copy, t.value)
    assert answer == t.result, str(t)

    assert t.nums == nums_copy, "You shouldn't change inputs"
%%time
for case in TEST_CASES:
    test_find_value(case)

Functions#

print("Welcome to Moscow!")
print("Welcome to Samara!")
print("Welcome to Novosibirsk!")
print("Welcome to Sochi!")
print("Welcome to Khabarovsk!")
# can write as a loop
for city in ['Moscow', 'Samara', 'Novosibirsk', 'Sochi', 'Khabarovsk']:
    print("Welcome to ", city, '!', sep='')
# and wrap into a function
def print_wellcome(location):
    print("Welcome to ", city, '!', sep='')

for city in ['Moscow', 'Samara', 'Novosibirsk', 'Sochi', 'Khabarovsk']:
    print_wellcome(city)

Typing#

import typing as tp


def f(
    int_value: int,
    float_value: float,
    str_value: str,
    list_value: list[str],
    tuple_value: tuple[str, str],
    optional_str_value: str | None,
    str_or_int_value: str | int,
    type_value: type,
    any_value: tp.Any
) -> None:
    pass

f(
    int_value=10, 
    float_value=10.1, 
    str_value=str, 
    list_value=["a", "b"],
    tuple_value=("a", "b"),
    optional_str_value="hello",  # or None
    str_or_int_value="world",  # or 1
    type_value=int,
    any_value=12345  # or any other type
)
Anti-pattern: not using type hints
# Newbie
def ctr(clicks, shows):
    return clicks / shows
    
# Expert
def ctr(clicks: int, shows: int) -> float:
    return clicks / shows
Anti-pattern: write functions with crazy inputs
# Tried to figure out what are the types of the arguments of this function...

def _update_input_fields(input_fields, output_fields):
    
    for output_field_name, output_field_type in dict(output_fields).iteritems():
        if output_field_name not in input_fields:
            input_fields[output_field_name] = output_field_type
        elif init_to_string(input_fields[output_field_name]) != init_to_string(output_field_type):
            input_fields[output_field_name] = _merge_field_type(input_fields[output_field_name], output_field_type)
# Done. Do not do like this

import typing as tp

def _update_input_fields(
        input_fields: tp.Mapping[str, tp.Union[tp.Tuple[tp.Union[tp.Callable[tp.Any, tp.Any], str], tp.Union[tp.Callable[tp.Any, tp.Any], str]], tp.Union[tp.Callable[tp.Any, tp.Any], str]]], 
        output_fields: tp.Union[tp.Mapping[str, tp.Union[tp.Tuple[tp.Union[tp.Callable[tp.Any, tp.Any], str], tp.Union[tp.Callable[tp.Any, tp.Any], str]], tp.Union[tp.Callable[tp.Any, tp.Any], str]]], tp.Sequence[tp.Tuple[str,  tp.Union[tp.Tuple[ tp.Union[tp.Callable[tp.Any, tp.Any], str],  tp.Union[tp.Callable[tp.Any, tp.Any], str]],  tp.Union[tp.Callable[tp.Any, tp.Any], str]]]]]
        ) -> None:
    
    for output_field_name, output_field_type in dict(output_fields).iteritems():
        if output_field_name not in input_fields:
            input_fields[output_field_name] = output_field_type
        elif init_to_string(input_fields[output_field_name]) != init_to_string(output_field_type):
            input_fields[output_field_name] = _merge_field_type(input_fields[output_field_name], output_field_type)
Anti-pattern: do not return values from some branches
# newbie
def ctr(clicks: int, shows: int) -> float | None:
    if shows != 0:
        return clicks / shows
    
# expert
def ctr(clicks: int, shows: int) -> float:
    if shows != 0:
        return clicks / shows
    return 0.0

Comments#

# comments begin with symbol #

def function():  # can write a comment here
    pass
Anti-pattern: duplicate obvious code with comments
def product_three(a, b, c):
    return a * b * c  # multiply three numbers in order to return the result
Anti-pattern: write comments instead of clear code
def f(u, b):
    return u * b

def g(ub, c):
    return ub * c

def h(c, b, u):  # the button a was broken on my keybord,
                 # and I just like the order of these variables
    # this function multiplies three numbers
    return g(f(c, b), u)  # first, multiply c and b;
                          # then pass the result to the function g,
                          # which multipies it with the third number 
Advice: A good comment:
1. is abscent because the clear code made it unnecessary
2. answers the question "why?"

Documentation#

def print_message(message: tp.Optional[str]):
    # Prints the passed message
    # Handles the cases of empty and missing messages separately
    if message is None:
        print("No message!")
    elif not message:
        print("Empty message!")
    else:
        print(message)
help(print_message)  # help – one more builtin
def print_message(message: tp.Optional[str]):
    """
        Prints the passed message.
        Handles the cases of empty and missing messages separately.
    """
    if message is None:
        print("No message!")
    elif not message:
        print("Empty message!")
    else:
        print(message)
print_message.__doc__
help(print_message)
print_message  # in Jupyter use shift + tab for docs
Anti-pattern: not to write docstrings
# newbie
def ctr1(clicks: int, shows: int) -> float:
    if shows != 0:
        return clicks / shows
    return 0.0
    
# expert
def ctr2(clicks: int, shows: int) -> float:
    """
    Calculate ctr. If there are no shows, return 0.
    :param clicks: number of clicks on banner
    :param shows: number of banners shows
    :return: clicks-through rate
    """
    if shows != 0:
        return clicks / shows
    return 0.0
Advice: document the nontrivial logic
Advice: document cautions

assert#

assert True  # assert
assert False # this always fails
assert 2 + 2 == 4  # assert expresses the confidence that the expression is always true 
                   # during the normal program execution. Mainly used for debugging
allways_filled_list = []
assert allways_filled_list, "List must not be empty!"
import typing as tp

def _dot_product(first: tp.Sequence[float], second: tp.Sequence[float]) -> float:  
    
    result = 0
    for i in range(len(first)):
        result += first[i] * second[i]
        
    return result


def length(vector: tp.List[float]) -> float:
    return _dot_product(vector, vector) ** 0.5


length([1, 2, 3, 4, 5])
import typing as tp

def _dot_product(first: tp.List[float], second: tp.List[float]) -> float:    
    assert len(first) == len(second), "First and second should be equal length"
    
    result = 0
    for el_first, el_second in zip(first, second):
        result += el_first * el_second
        
    return result


def length(vector: tp.List[float]) -> float:
    return _dot_product(vector, vector) ** 0.5


length([1, 2, 3, 4, 5])

assert in tests#

def dummy_function(a) -> str:
    return "hello"

def test_dummy_function(value) -> None:
    assert dummy_function(value) == value, "Function doesn't return the same value as passed"
test_dummy_function("hello")
test_dummy_function("hi")