- INDEX
Python is an interpreted language. Commands are executed through a piece of software known as the Python interpreter
- Python is an object-oriented language and
classesform the basis for all data types- Python is a dynamically typed language, as there is no advance declaration associating an identifier with a particular data type
-
To run a Python program, you must first have the Python interpreter installed on your computer. Then you use it to execute your program by typing the following command at the command line:
python program.py
- What this does is tell the Python interpreter to execute the program contained in the file
program.pywhich converts the program into bytecode (zeros and ones) that is then executed by the Python virtual machine.
- What this does is tell the Python interpreter to execute the program contained in the file
One of the oldest PEPs is PEP 8, which instructs Python programmers on how to style their code. The Python style guide was written with the understanding that code is read more often than it is written
The PEP 8 guidelines are not set in stone, and some teams prefer a different values. Don’t worry too much about them in your code as you’re working alone, but be aware that people who are working collaboratively almost always follow the PEP 8 guidelines
- Indentation (Whitespace)
- PEP 8 recommends that you use four spaces per indentation level. Using four spaces improves readability while leaving room for multiple levels of indentation on each line.
- Line Length
- each line should be less than 80 characters (79 Character limit)
- PEP 8 also recommends that you limit all of your comments to 72 characters per line, because some of the tools that generate automatic documentation for larger projects add formatting characters at the beginning of each commented line.
A Type is a classification of data that tells the computer possible values for that type and the operations that can be performed on it
Python has a number of built-in types, including integers, floating-point numbers, Booleans, sequences (such as strings, lists, tuples, and ranges), and dictionaries, as well as classes, instances, and exceptions. All instances of these types are objects.
In Python, everything is an object, and every object has a type
-
if you have a constant-number: (variable whose value stays the same throughout the life of a program), then Python doesn’t have built-in constant types, but Python programmers use all capital letters to indicate a variable should be treated as a constant and never be changed:
MAX_CONNECTIONS = 5000
/this is called "True Division" returning a floating-point result
-
floor division: also called integer-division (results in an integer by discarding the fraction part)
-
it rounds down to a small value ->
print(14 // 8) # 1` print(-14 // 8) # -2` print(-27 // 4) # -7`
-
floor dividing by 10s removes last digits
print(12345 // 10) # 1234` print(12345 // 1000) # 12`
-
-
modulus(remainder) operator
%returns the remainder-
module dividing by 10s gets last digits
print(12345 % 10) # 5` print(12345 % 1000) # 345` print(-27 % 4) # -1 (not 6)
-
-
multiple assignments
m = 3 m, n = 10 * m, m + 1 # 30, 4 (uses old value of m) # try not to do it complicated like that to avoid errors
-
operators precedence:
-2**2 # -4 (-2)**2 # 4
-
Relational operators:
- boolean operation are converted to integer
5 * (2 < 4) # 5 5 * (2 > 4) # 0 x = 2 x += 2 < 8 # 3
-
strings comparison: Based on English Dictionary (Letter by letter comparison)
# If a word has a smaller letter: it appears first print('love' < 'zebra') # True l is before z print('love' < 'long') # False: lo are common, but v > n print('love' != 'long') # True # If one word is done in comparison: the smaller in length comes first print('counter' < 'counterattack') # True # Upper letters are smaller than small letters print('A' < 'a') # True print('A' < 'z') # True print('Z' < 'a') # True print('loVE' < 'love') # True V < v print('loVE' < 'long') # True V < n print('' < 'A') # True empty is smaller print(' ' < 'A') # True: space smaller than letters print(' ' < 'a') # True: space smaller than letters print('0' < 'A') # True: Digits smaller than letters print('0' < 'a') # True: Digits smaller than letters
-
Float comparison try to avoid it
a = 3 / 7 # 0.42857142857142855 b = 0.1 + 3/7 - 0.1 # 0.4285714285714286 print(a == b) # False x = 5.0 y = 4.9999999999999999999999999999999999999 print(x == y) # True: y is approximated to 5
-
Extended Assignment Operators
-
For immutable types, we shouldn't assume that this syntax changes the value of the existing variable. Instead, it creates a new variable and assigns it to the existing variable name
x = 5 x += 1 # x = x + 1
-
For mutable types, we can assume that the syntax changes the value of the existing variable
x = [1, 2, 3] x += [4, 5] # x = x + [4, 5]
-
-
Membership (Containment check) operators ->
in,not inoperators- Prefix: any string starts from the first character (n prefixes)
ex: "ahmed omar" -> "ahme"ex: URL with the common prefixhttps://
- Suffix: any string ends from the last character (n Suffix)
ex: "ahmed omar" -> "d omar"
- Substring: starts and ends wherever, but consecutive
ex: "ahmed omar" -> "med oma"
- Sub-sequence: not consecutive, but must be in order (next letter must has bigger index)
ex: "ahmed omar" -> "amd"
- Prefix: any string starts from the first character (n prefixes)
-
identity operator ->
is/is not- returns True if two variables are the same object-reference in memory
- don't use it with immutable objects
- In most programming situations, the equivalence tests
==and!=are the appropriate operators; use ofisandis notshould be reserved for situations in which it is necessary to detect true "aliasing".
They are used to manipulate bits of numbers at the binary level.
-
AND ->
&1 & 1 = 11 & 0 = 00 & 1 = 00 & 0 = 0
-
OR ->
|1 | 1 = 11 | 0 = 10 | 1 = 10 | 0 = 0
-
XOR ->
^1 ^ 1 = 01 ^ 0 = 10 ^ 1 = 10 ^ 0 = 0
-
NOT ->
~~1 = 0~0 = 1
-
Left Shift ->
<<-> it shifts the bits to the left by the number of bits specified and adds0sto the end (right side)x << y->x * 2^y5 << 1->5 * 2^1->105 << 2->5 * 2^2->20
-
Right Shift ->
>>-> it shifts the bits to the right by the number of bits specified and adds0sto the beginning (left side)x >> y->x / 2^y5 >> 1->5 / 2^1->25 >> 2->5 / 2^2->1
-
Bitwise Operators Precedence:
~-><<->>>->&->^->|
-
Parity of a number: it's the number of
1sin the binary representation of that number- number of
1sis odd -> parity is1 - number of
1sis even -> parity is0
# 5 -> 101 -> 2 1s -> parity is 0 # 6 -> 110 -> 2 1s -> parity is 0 # 7 -> 111 -> 3 1s -> parity is 1
- number of
Python supports a conditional expression syntax that can replace a simple control structure. The general syntax is an expression of the form:
# expr1 if condition else expr2
param = n if n >= 0 else −nIt's a new feature in Python 3.10, it's a new way to write if-elif-else statements. It's similar to switch statement in other languages.
match expression:
case pattern1:
# do something
case pattern2:
# do something
case _: # default case
# do something
# Example:
n = 2
match n:
case 1:
print("one")
case 2:
print("two")
case 3:
print("three")
case _:
print("something else")- Note: No need to use
breakkeyword, it's automatically added
Python supports two types of loops: while and for loops.
- while loop : allows for repeated execution of a block of code as long as a condition is true.
- for loop : provides a more convenient way to iterate over a (sequence of values).
- There're statements that you can use to control the flow of a loop:
break-> to exit the loopNote:
returnis stronger thanbreak, it exits the function and not only the loopcontinue-> to skip the current iteration and continue to the next onepass-> to do nothing
while (condition):
# do somethingfor i in range(5):
print(i, end=' ') # 0 1 2 3 4
# How range works:
def range(start, stop=None, step=1):
if stop is None:
stop = start
start = 0
# ...-
range can be one number(0->number) or range (number1 -> number2) or you can also specify the value of each step
- last number in the range is not included
- if you used a negative value as a step, this means you decrease and not increase (iterating backwards)
-
enumerate function : returns an enumerated object
for i, value in enumerate(range(5, 8)): print(i, value) for i, value in enumerate('ali'): print(i, value)
-
for else-> loop that can have an optional (elseblock), the else part is executed if the items in the sequence used in for loop exhausts. (it can also be used withwhileloop)-
else block is not executed (ignored) if you break from the loop
for i in range(5): print(i) if i == 3: break else: print("for loop is finished")
-
Note: we can use a
forloop in cases for which awhileloop does not apply, such as when iterating through a collection, such as aset, that does not support any direct form of indexing.
An iterator is an object that manages an iteration through a series of values. If variable, i, identifies an iterator object, then each call to the built-in function, next(i), produces a subsequent element from the underlying series, with a StopIteration exception raised to indicate that there are no further elements.
- That iterator does not store its own copy of the list of elements. Instead, it maintains a current index into the original list, representing the next element to be reported.
- Therefore, if the contents of the original list are modified after the iterator is constructed, but before the iteration is complete, the iterator will be reporting the updated contents of the list.
An iterable is an object, obj, that produces an iterator via the syntax iter(obj).
-
For example, calling
iter(data)on a list instance (data) produces an instance of the list iterator classdata = [1, 2, 3] next(data) # ❌ TypeError: 'list' object is not an iterator i = iter(data) # ✅ print(next(i)) # 1
-
Python also supports functions and classes that produce an implicit iterable series of values, that is, without constructing a data structure to store all of its values at once.
- For example, the call
range(1000000)does not return a list of numbers; it returns a range object that is iterable. This object generates the million values one at a time, and only as needed. - Such a "lazy evaluation" technique has great advantage:
- In the case of
range, it allows a loop of the form,for j in range(1000000), to execute without setting aside memory for storing one million values. - Also, if such a loop were to be interrupted in some fashion, no time will have been spent computing unused values of the
range.
- In the case of
- For example, the call
-
We see "lazy evaluation" used in many of Python’s libraries. For example, the dictionary class supports methods
keys(),values(), anditems(), which respectively produce a “view” of all keys, values, or(key,value)pairs within a dictionary. None of these methods produces an explicit list of results. Instead, the views that are produced are iterable objects based upon the actual contents of the dictionary. -
Using
range()to Make a List of Numbers instead of iterables-
If you want to make a list of numbers, you can convert the results of
range()directly into a list using thelist()functionnumbers = list(range(1, 6))
-
-
An explicit list of values from such an iteration can be immediately constructed by calling the list class constructor with the iteration as a parameter
num_list = list(range(1000)) # produces a list instance with values from 0 to 999
-
next()- returns the next item from the iterator
i = iter([1, 2, 3]) print(next(i)) # 1 print(next(i)) # 2 print(next(i)) # 3 print(next(i)) # StopIteration
-
map()- returns an iterator that applies function to every item of iterable, yielding the results
def square(x): return x * x for val in map(square, range(1, 5)): print(val, end=' ') # 1 4 9 16
-
filter()- returns an iterator that includes only those items for which the function returns a true value
def is_even(x): return x % 2 == 0 for val in filter(is_even, range(1, 5)): print(val, end=' ') # 2 4
-
A generator is implemented with a syntax that is very similar to a function, but instead of returning values, a yield statement is executed to indicate each element of the series.
-
It's used to save memory, because it doesn't store all the values in memory, it generates them on the fly
The most convenient technique for creating iterators in Python is through the use of generators
def factors(n): # traditional function that computes factors
results = [ ] # store factors in a new list
for k in range(1,n+1):
if n % k == 0: # divides evenly, thus k is a factor
results.append(k) # add k to the list of factors
`return results # return the entire list
# ------------------------------------------------------
# implementation of a generator for computing those factors
def factors(n): # generator that computes factors
for k in range(1,n+1):
if n % k == 0: # divides evenly, thus k is a factor
yield k # yield this factor as next result- Notice use of the keyword
yieldrather thanreturnto indicate a result. This indicates to Python that we are defining a generator, rather than a traditional function.- It is illegal to combine
yieldandreturnstatements in the same implementation,
- It is illegal to combine
- How it works?
- For each iteration of the loop, Python executes our procedure until a
yieldstatement indicates the next value. At that point, the procedure is temporarily interrupted, only to be resumed when another value is requested. - When the flow of control naturally reaches the end of our procedure (or a
zero-argumentreturn statement), a "StopIteration" exception is automatically raised.
- For each iteration of the loop, Python executes our procedure until a
- the keyword
def, serves as the function’s "signature" - Each time a function is called, Python creates a dedicated activation record that stores information relevant to the current call. This activation record includes what is known as a "namespace" ) to manage all identifiers that have
local scopewithin the current call.- The namespace includes the function’s parameters and any other identifiers that are defined locally within the body of the function.
- If a
returnstatement is executed without an explicit argument, theNonevalue is automatically returned.- Likewise,
Nonewill be returned if the flow of control ever reaches the end of a function body without having executed a return statement.
- Likewise,
In documentations, when you find a
functionor amethod, have argument like this:method([argument]), this means that the argument is optional
-
Casting Types
int(3.14) # Output: 3 int(3.98) # Output: 3 int(-3.98) # Output: -3 int("3") # Output: 3 int("3.14") # Output: ValueError: invalid literal for int() with base 10: '3.14' # ------------------------------------------------------ float() # Output: 0.0 float("3") # Output: 3.0 float("3.14") # Output: 3.14
-
Get number for a Unicode character (Character Encoding)
-
ord()function -> returns an integer representing the Unicode character.# find unicode of P print(ord('P')) # Output: 80 print(chr(80)) # Output: 'P'
-
this is useful to understand how python compares lowerCase letter and upperCase letters
print("aaa" > "AAA") # True
-
Python provides means for functions to support more than one possible
calling signature. Such a function is said to be polymorphic (which is Greek for “many forms”).
-
Default Parameters
- they must be the last argument when defining the function
-
Parameter Naming types
-
positional arguments
- It's the default mechanism for matching the actual parameters sent by a caller
def f(a,b,c): print(a,b,c) f(1,3,2)
-
keyword arguments (in-order or out-of-order)
def f(a,b,c): print(a,b,c) f(a=1,c=2,b=3)
-
You can't use positional-argument after a keyword-argument
-
-
*args-> we can use the wildcard (*) notation to write functions that accept an Arbitrary number of arguments- it gathers all remaining arguments into a
tuplewith name "args" - it doesn't have to be called
*args, you can use any name, e.g.*jobs/*scores
def average(*args): total = 0 for arg in args: total += arg return total/len(args)
-
Mixing Positional and Arbitrary Arguments
-
If you want a function to accept several different kinds of arguments, the parameter that accepts an arbitrary number of arguments must be placed last in the function definition
def make_pizza(size, *toppings):
-
- it gathers all remaining arguments into a
-
**
**kwargs** -> we can use the wildcards notation to write functions that accept an Arbitrary number of keyword arguments (key-value pairs)- The double asterisks before the parameter cause Python to create a dictionary containing all the extra name-value pairs the function receives.
def print_ages(**kwargs):
for k,v in kwargs.items():
print(f"{k} is {v} years old")
print_ages(max=67, sue=59, kim= 14)
# max is 67 years old
# sue is 59 years old
# kim is 14 years oldPython provides integrated support for embedding formal documentation directly in source code using a mechanism known as a docstring. Formally, any string literal that appears as the first statement within the body of a module, class, or function (including a member function of a class) will be considered to be a docstring. By convention, those string literals should be delimited within triple quotes (”””)
def scale(data, factor):
”””Multiply all entries of numeric data list by the given factor."""
for j in range(len(data)):
data[j] = factor-
More detailed
docstringsshould begin with a single line that summarizes the purpose, followed by a blank line, and then further details.- It serves as documentation and can be retrieved in a variety of ways. For example, the command help(x), within the Python interpreter, produces the documentation associated with the identified object x
def scale(data, factor): ”””Multiply all entries of numeric data list by the given factor. data an instance of any mutable sequence type (such as a list) containing numeric elements factor a number that serves as the multiplicative factor for scaling ””” for j in range(len(data)): data[j] = factor
in every scope, a
NameErrorwill be raised if no such definitions are found. The process of determining the value associated with an identifier is known as "name resolution".
-
In python, there're 4 types of namespaces:
-
order of using a variable in a function: (python search order):
local->enclosing->global->build-in
-
in functional-scope you can't modify/write global variables
-
to do so global variable inside a function --> use
globalwordb = 20 # This won't work ❌ def f(): b = 5 # local variable print(b) # 5 # This will work ✅ def f(): global b b = 5 # now you can access & change global variables f() print(b) # 5
-
in block-scope (
if/loop) you can access global variables
-
-
A namespace is an abstraction that manages all of the identifiers that are defined in a particular scope, mapping each name to its associated value. In Python, functions, classes, and modules are all first-class objects, and so the “value” associated with an identifier in a namespace may in fact be a function, class, or module.
In the terminology of programming languages, first-class objects are instances of a type that can be assigned to an identifier, passed as a parameter, or returned by a function
It's a "data-type" that can be: assigned / passed / returned
-
In Python,
functionsandclassesare also treated as first-class objects. For example, we could write the following:scream = print # assign name ’scream’ to the function denoted as ’print’ scream( Hello ) # call that function
- an assignment such as
scream = print, introduces the identifier,scream, into the current namespace, with its value being the object that represents the built-in function,print - this demonstrates the mechanism that is used by Python to allow one function to be passed as a parameter to another
- an assignment such as
It's an "ordered" collection of data
-
It's a referential structure, as it technically stores a sequence of references to its elements
-
can hold different data types
-
we can use
inoperator to iterateprint(2 in my_list) # True / False for item in my_list: # code
-
you can duplicate lists using
*operator -
to create a list from a tuple ->
List()
-
add items to list
my_list = [1, 5, 10, 17, 2] # append: add one item to the end my_list.append('Hii') # 1 5 10 17 2 Hii # ------------------------------------------------- # # Extend the list by appending all the items from the iterable another_lst = [3, 1] # accepts an iterable (another_lst) and appends each item from that iterable to the end of the list my_list.extend(another_lst) # 1 5 10 17 2 Hii 3 1 #TypeError: 'int' object is not iterable #my_list.extend(2) # ------------------------------------------------- # # Insert an item at a given position # insert( index before which to insert the element, element to be inserted) my_list.insert(2, 'Wow') # 1 5 Wow 10 17 2 Hii 3 1
-
removing items from list
my_list = [1, 5, 10, 17, 2, 'Hii'] # pop removes the item at a specific index and returns it. # it's useful if you want to use the value of an item after removing it from a list print(my_list.pop()) # Hii - default last item # Now list is : 1 5 10 17 2 # .pop(index) -> removes the item at that index and returns it print(my_list.pop(3)) # 17 # Now list is : 1 5 10 2 # del removes the item at a specific index: del my_list[0] # 5 10 2 # "remove()" lets us remove by value and not index -> removes ONLY the first matching value, not a specific index: # If there’s a possibility the value appears more than once in the list, you’ll need to use a loop to make sure all occurrences of the value are removed my_list.remove(10) # 5 2 # ValueError: list.remove(x): x not in list #my_list.remove('Hei') # removes all items from the list my_list.clear()
- to delete selectively from a list, don't iterate forwards as there will be conflict in indexes if you delete element, instead iterate backwards
-
searching for item in list
my_list = [1, 15, 7, 'mostafa', 7, True, 0] # search and return the FIRST index print(my_list.index(7)) # 2 print(my_list.index(True)) # (any index of the first value that isn't false) -> 0 print(my_list.index(False)) # 6 #ValueError: 'Wow' is not in list #print(my_list.index('Wow')) my_list.clear() print(len(my_list)) # 0
-
Sorting
-
sortmethod -> Permanently & in-placemy_list = [5, 7, 2] # no new list -> in-place (memory-efficient) my_list.sort() # [2, 5, 7] # common mistake: my_list = my_list,sort() # NONE
-
sortedfunction -> Temporarily- returns a sorted list of a specified iterable object
my_list = [5, 7, 2] new_list = sorted(my_list) # [2, 5, 7] # my_list doesn't change new_string = sorted('zacb') # ['a', 'b', 'c', 'z'] # my_list doesn't change # common mistake: my_list = my_list,sort() # NONE
-
same for
reverse()method andreversed()function- which reverses a list in-place (reverse the main list and not returning a new list)
-
Notice that
reverse()doesn’t sort backward alphabetically; it simply reverses the order of the list
-
-
all&anyfunctions- return
true/falseif all/some elements of the iterable aretrue/false
- return
-
We want to subsequently be able to add additional colors to palette, or to modify or remove some of the existing colors, without affecting the contents of
warmtones. If we were to execute the commandpalette = warmtones
-
We can instead create a new instance of the list class by using the syntax:
palette = list(warmtones)
- Copying lists
- count
- the
.count()method returns the number of times a value occurs in a list - if the value is not in the list, it returns "0"
- the
It's like destructuring
-
It's usually used when passing parameters to a function
def sum(a, b, c): return a + b + c nums = [1, 2, 3] # Not recommended print(sum(nums[0], nums[1], nums[2])) # 6 # Recommended (unpacking) print(sum(*nums)) # 6
List comprehension offers a shorter syntax when you want to create a new list based on the values of an existing list.
[expression for value in iterable if condition]
# equal to:
result = [ ]
for value in iterable:
if condition:
result.append(expression)lst1 = [2, 3, 4, 1]
# Old syntax
lst2 = []
for i in lst1:
lst2.append(i *i + 1)
print(lst2) # [5, 10, 17, 2]
# new syntax
lst2 = [i*i+1 for i in lst1]
print(lst2) # [5, 10, 17, 2]Python supports similar comprehension syntaxes that respectively produce a
set,generator, ordictionary[ k k for k in range(1, n+1) ] list comprehension { k k for k in range(1, n+1) } set comprehension ( k k for k in range(1, n+1) ) generator comprehension { k:k k for k in range(1, n+1) } dictionary comprehension
Lists work well for storing collections of items that can change throughout the life of a program. However, sometimes you’ll want to create a list of items that cannot change. Tuples allow you to do just that. Python refers to values that cannot change as immutable, and an immutable list is called a tuple.
They're immutable, ordered, indexed collections/sequences
-
If you want to define a tuple with one element, you need to include a trailing comma at the end

t = (10) # wrong t = (10,) # right # notice the different (string vs tuple) print(('Hi') * 4) # HiHiHiHi print(('Hi',) * 4) # ('Hi', 'Hi', 'Hi', 'Hi')
-
you can't mutate the items in the tuple unlike lists
-
automatic packing of a tuple of a tuple:
-
If a series of comma-separated expressions are given in a larger context, they will be treated as a single tuple, even if no enclosing parentheses are provided.
-
One common use of packing in Python is when returning multiple values from a function
return x, y # returning a single object that is the tuple (x, y)
-
-
automatic unpacking:
- allowing one to assign a series of individual identifiers to the elements of sequence. - side expression can be any iterable type, as long as the number of variables on the left-hand side is the same as the number of elements in the iteration
a, b, c, d = range(7, 11) # a=7, b=8, c=9, and d=10 # This technique can be used to unpack tuples returned by a function quotient, remainder = divmod(a, b) # This syntax can also be used in the context of a for loop for x, y in [ (7, 2), (5, 8), (6, 4) ]:
-
Simultaneous Assignments
- The combination of
automatic packingandunpackingforms a technique known assimultaneous assignment, whereby we explicitly assign a series of values to a series of identifiers, using a syntax:
x, y, z = 6, 2, 5
-
It provides a convenient means for swapping the values associated with two variables:
j, k = k, j
- With this command,
jwill be assigned to the old value ofk, andkwill be assigned to the old value ofj. Withoutsimultaneous assignment, a swap typically requires more delicate use of atemporary variable
- With this command,
- The combination of
-
unpacking: to make a variable equal the rest of element from the beginning or the end or the middle, this is done using the astrict
*operatora, b, *c = 1, 2, 3, 4, 5 print(c) # [3, 4, 5]
-
Zip function : when you have multiple sequences and want to iterate such that in each iteration you have a single item
numbers = [1, 2, 3] letters = ['a', 'b', 'c'] # zip class constructor: def __init__(self, *iterables) zipped = zip(numbers, letters) print(list(zipped)) # [(1, 'a'), (2, 'b'), (3, 'c')]
They're unordered collections with no duplicate elements
-
Why use sets ?
- sets make it easy/fast to check if a value exists in a collection
- as in
liststhere's ahashingand other operations that may slow operation compare tosets
- as in
- sets are an easy way to remove duplicates from a collection
- sets make it easy/fast to check if a value exists in a collection
-
can contain only immutable elements (no
lists/dictionaries/setswhich are mutable-types) --> so that it contains only unique elements -
to create an empty set, you must write it like this:
empty_set = {} # wrong -> will be declared as a dictionary not a set empty_set = set() # correct
set('hello') # {'e', 'h', 'l', 'o'}
# add item
even = {2, 4, 6}
even.add(8)
# Remove item
even.remove(6)
even.remove(3) # error
even.discard(3) # no error-
Subset and Superset
s1 <= s2 # s1 is a subset of s2 s1 < s2 # s1 is a proper subset of s2 s1 >= s2 # s1 is a superset of s2 s1 > s2 # s1 is a proper superset of s2
you can use single or double quotes
"""
dsfs
'dsfdsf'
sf
"fff"
"""
# or
'''
dsfdsf
sdfdf
'''-
.find(): if doesn't find the text it returns-1.rfind(): same asfind()but returnsValueErrorif not found
-
removePrefix():- Prefix: any string starts from the first character (n prefixes)
ex: "ahmed omar" -> "ahme"ex: URL with the common prefixhttps://- When you see a URL in an address bar and the
https://part isn’t shown, the browser is probably using a method likeremoveprefix()behind the scenes.
- When you see a URL in an address bar and the
- Prefix: any string starts from the first character (n prefixes)
name, age = 'mostafa', 33
print(name, 'is', age, 'years old') # 1 old way
print(name + ' is ' + str(age) + ' years old') # 2 old way
# The first {} is replaced with mostafa
# the 2nd is replaced with 33
print('{} is {} years old'.format(name, age)) # mostafa is 33 years oldThe f is for format, because Python formats the string by replacing the name of any variable in braces with its value. The output from the previous code is:
# new way:
name, age = 'mostafa', 33
print(f'{name} is {age} years old') # mostafa is 33 years old
# old way:
# we call this string with curly braces {} as template
#IndexError: tuple index out of range - u need to provide 3 arguments
#print('{}{}{}'.format('Hey'))
print('{}{}{}'.format(1, 2, 3, 4, 5, 6)) # 123 - OK to provide more. Ignored-
Formatting with replacements fields
name, age = 'mostafa', 33 print('{0} is {1} years old'.format(name, age)) # mostafa is 33 years old #print('{0} is {2} years old'.format(name, age)) # IndexError - no idx 2 print('{0} is {1} years old. Are you {1} years as {0}'.format(name, age)) # mostafa is 33 years old. Are you 33 years as mostafa # pros: you provie positional argument once and use it many print('{name} is {AGE} years old. Are you {AGE} years as {name}'.format(name=name, AGE=age)) # mostafa is 33 years old. Are you 33 years as mostafa # similarly, we can use keyword arguments but flxible order! # Be careful from mixing print('{} is {age} years old'.format(name, age=age)) # mostafa is 33 years old print('{0} is {age} years old'.format(name, age=age)) # mostafa is 33 years old #print('{1} is {age} years old'.format(age=age, name)) # SyntaxError: positional argument follows keyword argument #print('{1} is {age} years old'.format(name, age=age)) # IndexError
-
Formatting with specified width of text place
-
right aligning
for i in range(0, 20, 2): print('Given i={:2}: i^4 = {:7} i^3 = {:4}'.format(i, i* i * i * i, i * i * i)) """ {1:7}: Format position 1 in field of 7 spaces Given i= 0: i^4 = 0 i^3 = 0 Given i= 2: i^4 = 16 i^3 = 8 Given i= 4: i^4 = 256 i^3 = 64 Given i= 6: i^4 = 1296 i^3 = 216 Given i= 8: i^4 = 4096 i^3 = 512 Given i=10: i^4 = 10000 i^3 = 1000 Given i=12: i^4 = 20736 i^3 = 1728 Given i=14: i^4 = 38416 i^3 = 2744 Given i=16: i^4 = 65536 i^3 = 4096 Given i=18: i^4 = 104976 i^3 = 5832 """
-
left aligning
for i in range(0, 20, 2): print('Given i={:<2}: i^4 = {:<7} i^3 = {:<4}'.format(i, i* i * i * i, i * i * i)) """ Using :<7 makes it left-aligned Given i=0 : i^4 = 0 i^3 = 0 Given i=2 : i^4 = 16 i^3 = 8 Given i=4 : i^4 = 256 i^3 = 64 Given i=6 : i^4 = 1296 i^3 = 216 Given i=8 : i^4 = 4096 i^3 = 512 Given i=10: i^4 = 10000 i^3 = 1000 Given i=12: i^4 = 20736 i^3 = 1728 Given i=14: i^4 = 38416 i^3 = 2744 Given i=16: i^4 = 65536 i^3 = 4096 Given i=18: i^4 = 104976 i^3 = 5832 """
-
formatting precision ->
{value:width.precision}val = 71.01234567890123456789012345678901234567890123456789 print(val) #71.01234567890124 ==> 14 decisimal precision printed print('{:20}'.format(val)) # 71.01234567890124 ==> total 20 output units, right-aligned print('{:11f}'.format(val)) # 71.012346 ==> print 11 units. Use default precision (typically 6) print('{:11.3f}'.format(val)) # 71.012 ==> 11 output units, 3 of them precision print('{:3.5f}'.format(val)) #71.01235 ==> 5 precision. It will have more priority print('{:.8f}'.format(val)) #71.01234568 ==> 8 precision. No specific alignments #print('{.8f}'.format(val)) #AttributeError val = 2.67 print(val) #2.67 print('{:11f}'.format(val)) # 2.670000 ==> trailing zeros : 11 output units (6 is precision) print('{:11.2f}'.format(val)) # 2.67 (.2f use 2 precision) print('{:11.1f}'.format(val)) # 2.7 rounding print('{:11.0f}'.format(val)) # 3 rounding print('{:11.0f}'.format(2.5)) # 2 rounding to 2 print('{:11.0f}'.format(-2.5)) # -2 rounding to -2
-
This is called F-string
name, age = 'mostafa', 33
# mostafa is 33 years old
print(f'{name} is {age} years old')
val = 71.0123456789012345678901234
# 71.012
print(f'{val:11.3f}')- be aware of shallow-copying
def get_neibghours(i, j):
# {down, right, up, left};
di = [1, 0, -1, 0]
dj = [0, 1, 0, -1]
return [(i+di[d], j+dj[d]) for d in range(4)]
print(get_neibghours(0, 0))
# [(1, 0), (0, 1), (-1, 0), (0, -1)]
print(get_neibghours(3, 6))
# [(4, 6), (3, 7), (2, 6), (3, 5)]It's a key-value pairs, unlike lists which are index-value pairs
- Dictionaries, like sets, do not maintain a well-defined order on their elements. Furthermore, the concept of a subset is not typically meaningful for dictionaries, so the dict class does not support operators such as
<. Dictionaries support the notion of equivalence, with d1 == d2 if the two dictionaries contain the same set of keyvalue pairs.
They return something that iterable (NOT a list)
To convert them to lists, use the built-in method:
list()
-
Getting values from
.get()-
when you want to check if a key exists in the dictionary, we use
infirst then get the value -
instead, you can use the
.get()method, which will look for a given key in a dictionary. if the key exists, it will return the corresponding value. otherwise it returnsNone- unlike using the bracket-syntax which raises a KeyError
-
The
get()method requires a key as a first argument. As a second optional argument, you can pass the value to be returned if the key doesn’t exist:alien_0 = {'color': 'green', 'speed': 'slow'} alien_0.get('points', 'No point value assigned.')
-
Note: this is a good trick to use when looping on a dictionary keys count each key value
word = "ssdfdfsdsf" count = {} for char in word: count[char] = 1 + count.get(char,0) # this is instead of this: # if not count[char]: count[char] = 0 # count[char] += 1
-
-
-
Join (concatenate) 2 dictionaries
-
Similar to List Unpacking, we can unpack a dictionary into a series of identifiers using the
**operator- It maps the keys of the dictionary to the identifiers, and the values of the dictionary to the values of the identifiers
def func(a, b, c): print(a, b, c) d = {'a': 1, 'b': 2, 'c': 3} func(**d) # 1 2 3
Exceptions are unexpected events that occur during the execution of a program. An exception might result from a logical error or an unanticipated situation. In Python, exceptions (also known as errors) are objects that are raised (or thrown) by code that encounters an unexpected circumstance.
- When an error occurs in your program, the Python interpreter does its best to help you figure out where the problem is. The interpreter provides a traceback when a program cannot run successfully.
-
A traceback is a record of where the interpreter ran into trouble when trying to execute your code.
-
- Python includes a rich hierarchy of exception classes that designate various categories of errors

| Class | Description |
|---|---|
Exception |
A base class for most error types |
AttributeError |
Raised by syntax obj.foo, if obj has no member named foo |
EOFError |
Raised if “end of file” reached for console or file input |
IOError |
Raised upon failure of I/O operation (e.g., opening file) |
IndexError |
Raised if index to sequence is out of bounds |
KeyError |
Raised if nonexistent key requested for set or dictionary |
KeyboardInterrupt |
Raised if user types ctrl-C while program is executing |
NameError |
Raised if nonexistent identifier used |
StopIteration |
Raised by next(iterator) if no element |
TypeError |
Raised when wrong type of parameter is sent to a function |
ValueError |
Raised when parameter has invalid value. e.g. sqrt(−5) or int("cat") |
ZeroDivisionError |
Raised when any division operator used with 0 as divisor |
-
You can raise your own exceptions(force them to happen) whenever you want, using the
raisekeywordraise ValueError
-
You can provide a specific message when raising an exception
raise ValueError("invalid character")
def sqrt(x):
if not isinstance(x, (int, float)):
raise TypeError("x must be numeric")
if x < 0:
raise ValueError("x can't be negative")
# ... compute the square root of x ...There are several philosophies regarding how to cope with possible exceptional cases when writing code
-
One philosophy for managing exceptional cases is to “look before you leap (LBYL)”
-
The goal is to entirely avoid the possibility of an exception being raised (avoid leaping into the logic) through the use of a proactive conditional test.

if y != 0: ratio = x / y else: #... do something else ... # ------------------------------------------------ # or (input validation) inpdata = input("Enter a number > ") if not isinstance(inpdata,int): print("input value must be integer")
-
-
A second philosophy, often embraced by Python programmers (the "pythonic" way), is that “it is easier to ask for forgiveness than it is to get permission (EAFP)”
-
This philosophy is implemented using a
try-exceptcontrol structuretry: ratio = x / y except ZeroDivisionError: # ... do something else ...
-
The relative advantage of using a
try-exceptstructure is that the non-exceptional case runs efficiently, without extraneous checks for the exceptional condition. -
the
try-exceptclause is best used when there is reason to believe that the exceptional case is relatively unlikely, or when it is prohibitively expensive to proactively evaluate a condition to avoid the exception.
-
It's best used when there is reason to believe that the exceptional case is relatively unlikely, or when it is prohibitively expensive to proactively evaluate a condition to avoid the exception.
It is significantly easier to attempt the command and catch the resulting error than it is to accurately predict whether the command will succeed.
-
usually it's better to except a specific exception and handle it, rather than handling any possible exception that could occur
try: num = int(input("Enter a number: ")) except ValueError: print("Oh no, that ins't a number!")
-
you can specify the error in the handling:
try: fp = open( sample.txt ) except IOError as e: print( Unable to open the file: , e) # here we pass the error ("e")
-
You can also do multiple exceptions for multiple errors
age = int(input( Enter your age in years: )) while age <= 0: try: age = int(input( Enter your age in years: )) if age <= 0: print( Your age must be positive ) except (ValueError, EOFError): # multiple errors print( Invalid response )
-
Note: Notice that the lines flow order matters when handling exertions:
-
Here, the
ValueErrorwill be handled first, before the assignment toxis attempted. So,xwill be undefined if aValueErroroccurs. which will lead toNameErrortry: x = int(input("Enter a number: ")) except ValueError: print("That was not a valid number") print(x) # NameError: name 'x' is not defined
-
But here, we use the variable
xbefore theValueErroris handled, so it will be definedtry: x = int(input("Enter a number: ")) print(x) except ValueError: print("That was not a valid number")
-
-
Excepting without a specific error
-
You can except without a specific error, but it's not recommended as it will catch all errors, even the ones you didn't expect (Bad practice)
try: x = int(input("Enter a number: ")) except: print("That was not a valid number")
-
-
a
finallyclause, with a body of code that will always be executed in the standard or exceptional cases, even when an uncaught or re-raised exception occurs.- That block is typically used for critical cleanup work, such as closing an open file.
-
Exception handling is particularly useful when working with
user input, or whenreading from or writing to files, because such interactions are inherently less predictable. -
The keyword,
pass, is a statement that does nothing, yet it can serve syntactically as a body of a control structure. In this way, we quietly catch the exception, thereby allowing the surrounding while loop to continue.except (ValueError, EOFError): pass
Python is a dynamically typed language, which means that the type of a variable is inferred from its value at runtime. This is in contrast to statically typed languages, such as Java, where the type of a variable is declared explicitly in the source code.
-
We can use Type Hinting to specify the type of a variable, function parameter, or function return value. This is done by adding a colon after the variable name, followed by the type, as in:
# variable x: int = 5 # function def add(a: int, b: int) -> int: return a + b # class class Person: name: str age: int def __init__(self, name: str, age: int): self.name = name self.age = age
-
Note: Type annotations are not enforced by the Python interpreter. They are simply hints to the programmer and tools such as linters and type checkers.
-
So, you can still assign a value of a different type to a variable that has a type annotation. The following code will run without any errors:
x: int = 5 x = "hello"
-
Beyond the built-in definitions, the standard Python distribution includes perhaps tens of thousands of other values, functions, and classes that are organized in additional libraries, known as modules, that can be imported from within a program.
"module" is a collection of closely related functions and classes that are defined together in a single file of source code.
-
Python’s import statement loads definitions from a module into the current namespace. One form of an
importstatement uses a syntax such as the following:from math import pi, sqrt # adds both "pi" and "sqrt", as defined in the math module, into the current namespace from random import randint, choice # adds both "randint" and "choice", as defined in the random module, into the current namespace
-
If there are many definitions from the same module to be imported, an
asterisk (*)may be used as a wild card, as in:from math import *
but this form should be used sparingly. The danger is that some of the names defined in the module may conflict with names already in the current namespace (or being imported from another module), and the import causes the new definitions to replace existing ones.
-
Another approach that can be used to access many definitions from the same module is to import the module itself, using a syntax such as:
import math # adds the identifier, math, to the current namespace, with the module as its value
- Modules are also first-class objects in Python. Once imported, individual definitions from the module can be accessed using a fully-qualified name, such as
math.piormath.sqrt(2).
- Modules are also first-class objects in Python. Once imported, individual definitions from the module can be accessed using a fully-qualified name, such as
-
It is worth noting that top-level commands with the module source code are executed when the module is first imported, almost as if the module were its own script.
-
There is a special construct for embedding commands within the module that will be executed if the module is directly invoked as a script, but not when the module is imported from another script. Such commands should be placed in a body of a conditional statement of the following form:
# in the module file, ex: utility.py if __name__ == __main__ : # module
- commands will be executed if the interpreter is started with a command python utility.py, but not when the utility module is imported into another context.
- This approach is often used to embed
unit testswithin the module
-
python comes with tons of build-in modules that we can use, if we import them
-
each module consists of methods and functionality bundled together
-
Ex: Methods supported by instances of the Random class:

# use import statement to import modules import random r = random.randint(1,10) # use from-import statements to import functions from random import randint r = randint(1,10) # use from module import * statement to import all functions from random import * r = randint(1,10) 3
-
-
It's for creating your custom modules files
# utils.py def add(a, b): """This program adds two numbers and return the result""" result = a + b return result
-
pip: is the python package installer that we can use to install hundreds of thousands of packages for use in our projects
-
to install a package, use:
python3 -m pip install <package_na me>
-
for variables names, the "Pythonic" way is to use "snake_case", unlike in
Javascriptwhere we usually use "camelCase":first_name = 'Ahmed'
-
when you have 2 variables point to the same memory location, the second variable is called an "alias"
temperature = 23 original = temperature # original is called -> "alias"
-
print()can have other optional parameters like:-
end='end'Optional. Specify what to print at the end. Default is'\n'(line feed)# make separator "," instead of new line "\n" print(x, end=',')
-
sep='separator'Optional. Specify how to separate the objects, if there is more than one. Default is ' ' (space).# make separator "," instead of space " " print(x, y, z, sep=',')
-
-
del-> removes the bind from name of variable to the value in memory, so the variable will equalundefine(unboundLocalError)del alien_0['points']
-
object memory location -> to get identifier (location in memory) -> use built-in method
id()id()method is used to return a unique id for the specified object -> object memory address
- when we have different variables pointing to the same address --> Alias
- every thing in python is an object (mutable (reference) / immutable (primitive))
- Immutable objects

- Python interpreter already maintains what are known as "reference counts" for each object; this count is used in part to determine if an object can be garbage collected.
-
We can pass parameters when running a python file using
sysmodulepython3 myscript.py arg1 arg2 arg3
# sys.argv is a list of arguments passed to the python script # sys.argv[0] is the name of the script itself # sys.argv[1] is the first argument # sys.argv[2] is the second argument # argv -> argument vector
-
There's a library called
argparsethat is used to parse arguments passed to the scriptimport argparse parser = argparse.ArgumentParser() parser.add_argument("echo", help="echo the string you use here") args = parser.parse_args() print(args.echo)
python3 prog.py --help usage: prog.py [-h] echo
-









































