Python is one of the most popular and flexible programming languages, used for everything from basic scripting to machine learning.
Python stores integers and floating point numbers differently, as an int or float.
If you do calculations with an int and a float, Python will return a float.
| Symbol | Name |
|---|---|
| + | Addition |
| - | Subtraction |
| * | Multiplication |
| / | Divison |
| ** | Exponentiation |
| % | Modulo |
| // | Integer Division |
In Python division returns a float by default if applicable: 1/2 returns 0.5.
Exponentiation raises a number to a given power:
2 ** 3 # 8
49 ** 0.5 # 7.0Modulo divides two numbers and returns the remainder. It's often used to check if a number is even or odd.
10 % 3 # 1
16 % 2 # 0Integer division will return a floored int:
10/3 #3.33333333
10//3 #3Comments in Python files start with a #, # more of a comment than a question.
Variables are named symbols that hold a value.
Variables must be assigned before they can be used.
Assigning a variable: x = 100
Variables can be reassigned at any time.
You can assign multiple variables at the same time.
all, at, once = 5, 10, 15- Variables must start with a letter or underscore
- The rest of the name must consist or letters, numbers or underscores
- Names are case sensitive
- Most variables should be snake_case
- Most variables should be lowercase
- CAPITAL_SNAKE_CASE ususally refers to constants, for example PI
- UpperCamelCase usaully refers to a class
- Variables that start and end with two underscores ("dunder") are supposed to be private and left alone:
__no_touchy__
| Data Type | Description |
|---|---|
| bool | True or False values |
| int | an integer (1,2,3) |
| str | a sequence of unicode characters |
| list | an ordered sequence of values [1,2,3] |
| dict | a collection of key value pairs {"first_name": "Gerard"} |
Unlike statically-typed languages, Python lets you reassign variables to different types.
example = "a Bernese Mountain Dog"
example = 7None is Python's version of null, representing nothingness.
String literals can be declared with either single or double quotes. Be consistent.
In Python, there are escape sequences, all starting with a backslash, which are interpreted according to specific rules.
| Escape Sequence | Meaning |
|---|---|
| \n | new line |
| \ | backslash |
| " | doublequote |
| ' | singlequote |
You can concatenate strings with the "+" operator. If you try to concatenate a number and string, you'll get a TypeError.
You can update a string using the "+=" operator.
str_one = "ice"
str_one += " cream"
str_one # "ice cream"Python lets you interpolate variables into strings. Since Python 3.6 you can use F-Strings:
x = 10
formatted_str = f"I've told you {x} times already!"Before Python 3.6, the format method was common:
x = 10
formatted_str = "I've told you {} times already!".format(x)You can access individual string characters using square brackets and the characters index: "yes"[0] # 'y'
In string interpolation, data types are implicitly coverted into string form.
You can explicitly convert variables by using the name of the built-in type as a function.
decimal = 3.14159
integer = int(decimal) # 3
my_list = [1, 2, 3]
my_list_as_string = str(my_list) # "[1, 2, 3]"Python has a built in function, input, to prompt the user and store the result in a variable.
name = input()
if name == "Arya Stark":
print("Stabby stabby")
elif name == "John Snow":
print("*Indecision*")
else:
print("Carry on")In Python, all conditional checks resolve to True or False.
Besides False conditional checks, other things that are naturally falsy include: empty objects, empty strings, None, and zero.
| Operator | What it does |
|---|---|
| == | Truthy if a has the same value as b |
| != | Truthy if a does not have the same value as b |
| > | Truthy if a is greater than b |
| < | Truthy if a is less than b |
| >= | Truthy if a is greater than or equal to b |
| <= | Truthy if a is less than or equal to b |
| Operator | What it does |
|---|---|
| and | Truthy if both a and b are true |
| or | Truthy if either a or b are true |
| not | Truthy if the opposite of a is true |
In Python == and is are similar but not the same.
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True
a is b # False
c = b
b is c # True== checks if the variables' values are the same. is checks if the variables are stored in the same place in memory.
In Python, nested if statements are constructed through indentation:
age = input("How old are you?: ")
if age:
age = int(age)
if age >= 21:
print("You are good to enter and can drink")
elif age >= 18:
print("You can enter but need a wristband")
else:
print("You can't come in little one! :(")
else:
print("Please enter an age")Loops let you iterate over a collection of data and perform an action on each item.
for loops are written like this:
for item in iterable_object:
# do something with itemHere, item is a variable representing the current position of the iterator within the iterable. It will run through every item in the collection and then go away.
ranges are a way to generate a sequence of numbers, commonly used in for loops.
times = input("How many times do I have to tell you? ")
times = int(times)
for times in range(times):
print("CLEAN UP YOUR ROOM!")The third paramter, step, tells your code how many numbers to skip.
range(7)gives you the integers from 0 to 6range(1,8)gives you the integers from 1 to 7range(1,10,2)gives you odd integers from 1 to 10range(7, 0, -1)gives you the integers from 7 to 1
While loops will continue to execute while a condition is truthy.
while im_tired:
# seek caffeine
user_response = None
while user_response != "please":
user_response = input("Ah ah ah, you didn't say the magic word: ")The break keyword lets you immediately exit out of a loop.
A list is a collection of values in an ordered sequence. (It's an array)
The len function gives you the length of a list.
tasks = ["Install Python", "Learn about lists", "Take a break"]
len(tasks) # 3You can turn a range into a list using list.
tasks = list(range(1,4)) # [1, 2, 3]Like ranges, lists are zero-indexed. You can use a negative number to index backwards (-1 will give you the list item).
The syntax for iterating over a list with a for loop is very simple.
colors = ["purple", "teal", "magenta"]
for color in colors:
print(color)With a while loop, it's:
numbers = [1, 2, 3, 4]
i = 0
while i < len(numbers):
print(numbers[i])
i += 1append adds one item to the end of a list.
nums = [1, 2, 3]
nums.append(4) # [1, 2, 3, 4]extend adds all of the values you pass it to the end of a list.
nums = [1, 2, 3, 4]
nums.append([5, 6, 7]) # [1, 2, 3, 4, 5, 6, 7]insert inserts an item at a given index in a list.
todos = ["clean house", "exercise", "call family"]
todos.insert(2, "take out bins")
# ["clean house", "exercise", "take out bins", "call family"]clear removes all items from a list.
nums = [1, 2, 3]
nums.clear() # []pop removes the item at a given index and returns it. If you don't specify an index, pop removes and returns the last item in the list.
nums = [1, 2, 3, 4]
nums.pop() # 4
nums.pop(1) # 2remove removes the first item that matches a specified value. It will throw a ValueError if the item is not found.
nums = [1, 2, 3, 4, 4, 4, 5]
nums.remove(2) # [1, 3, 4]
nums.remove(4) # [1, 3, 4, 4, 5]index returns the index of a specified item in a list. You can specify a start and end point.
nums = [5, 6, 7, 8]
nums.index(6) # 1
nums2 = [5, 5, 6, 7, 5, 8, 8, 9, 10]
nums2.index(5,1) # 1
nums2.index(8,6,8) # 6count returns the number of times a specified value occurs in a list.
nums = [1, 2, 3, 4, 3, 2, 5, 6, 7, 8, 9, 10, 2]
nums.count(2) # 3
nums.count(3) # 2reverse will reverse the elements of a list (in-place).
nums = [1, 2, 3, 4]
nums.reverse() # [4, 3, 2, 1]sort sorts the items of a list (in-place).
nums = [4, 1, 2, 5, 3]
nums.sort() # [1, 2, 3, 4, 5]join (technically a string method) takes an iterable as an argument, concatenates a copy of the base string between each item of the iterable, and returns a new string.
words = ["Python", "is", "fun"]
" ".join(words) # "Python is fun"Slices copy part or all of a list and makes a new list.
some_list[start:end:step]If you enter a negative number, it will start the slice that many indices from the end.
The end parameter uses exclusive counting. Negative numbers tell you how many items to exclude from the end.
The step gives you the interval to skip. When the step is a negative number, reverse the order.
first_list[1, 2, 3, 4]
first_list[1:] # [2, 3, 4]
first_list[3:] # [4]
first_list[-1:] # [4]
first_list[:2] # [1, 2]
first_list[1:3] # [2, 3]
first_list[1:-1] # [2, 3]
first_list[::2] # [1, 3]
first_list[1::-1] # [2, 1]Slices can reverse a list or string.
string = "This is fun!"
string[::-1]Slices let you modify specific portions of a list.
nums = [1, 2, 3, 4, 5]
nums[1:3] = ["a", "b", "c"] # [1, "a", "b", "c", 4, 5]You can swap values in a list, for example if you want to shuffle or sort the list.
names = ["James", "Michelle"]
names[0], names[1] = names[1], names[0]
print(names) # ["Michelle", "James"]List comprehension lets you take an existing list and output another list with different values but based on the first list.
The syntax is:
[___ for ___ in ___]For example:
nums = [1, 2, 3]
[x*10 for x in nums] # [10, 20, 30]You can use list comprehension to manipulate strings and ranges too.
name = "lisa"
[char.upper() for char in name] # ["L", "I", "S", "A"]
[num*10 for num in range(1,6)] # [10, 20, 30, 40, 50, 60]List comprehension can be a concise way to covert one data type into another.
numbers = [1, 2, 3, 4, 5]
string_list = [str(num) for num in numbers] # ["1", "2", "3", "4", "5"]You can combine list comprehension with conditional logic.
numbers = [1, 2, 3, 4, 5, 6]
evens = [num for num in numbers if num % 2 == 0]
odds = [num for num in numbers if num % 2 != 0]numbers = [1, 2, 3, 4, 5, 6]
[num*2 if num % 2 == 0 else num/2 for num in numbers]
# [0.5, 4, 1.5, 8, 2.5, 12]Lists can contain any other kind of element, even other lists.
Nested lists are used in a variety of scenarios:
- complex data structures - matrices
- rows and columns for visualizations, tabulations and grouping data
- mazes, game boards
nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
nested_list[0][1] # 2
nested_list[1][-1] # 6You can use nested loops to output values from nested lists.
nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for el in nested_list:
for val in el:
print(val)You can also use list comprehension.
nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[print(val) for val in el] for el in nested_list]A dictionary is a data structure that consists of key value pairs. The keys describe the data, the values represent the data.
You can create a dictionary by assigning it directly to a variable, or by using the dict function.
dog = {
"name": "Buddy",
"is_cute": True
}
cat = dict(name="Pangur Ban", age=4)Like with lists, you can access a dictionary's values using [].
instructor = {"name": "Colt"}
instructor["name"] # ColtThe values method gives you an iterable collection of a dictionary's values dict_values that you can then run through with a for loop.
The keys method does the same thing for the dictionary's keys, creating dict_keys.
instructor = {
"name": "Colt",
"courses": 6,
"effective": True
}
for value in instructor.values():
print(value)
for key in instructor.keys():
print(key)The ìtems method gives you both as dict_items.
instructor = {
"name": "Colt",
"courses": 6,
"effective": True
}
for key,value in instructor.items():
print(key,value)You can use in to check if a key is in a dictionary.
"phone" in instructor # FalseTo check for a value, you can use in and the values method.
"Colt" in instructor.values() # Trueclear will clear all the kays and values in a distionary.
copy will make a copy of a dictionary.
d = {
"a":1,
"b":2,
"c":3
}
c = d.copy()
c == d # True
c is d # Falsefromkeys generates key-value pairs from comma seperated values. It can be used to generate default values.
{}.fromkeys(["name", "bio"], "") # {"name": "", "bio": ""}get retrieves a key in an objetc and returns None instead of KeyError if the key doesn't exist.
d = dict(a=1,b=2,c=3)
d["a"] # 1
d.get("a") # 1
d["no_key"] # KeyError
d.get("no_key") # Nonepop takes a single argument corresponding to a key and removes that key-value pair from the dictionary. It returns the value corresponding to the key that was removed.
d = dict(a=1,b=2,c=3)
d.pop() # TypeError
d.pop("a") # 1
d = {"b":2, "c":3}
d.pop("e") # KeyErrorpopitem removes a random key in a dictionary.
d = dict(a=1,b=2,c=3,d=4,e=5)
d.popitem() # ("d", 4)
d.popitem("a") # TypeErrorupdate updates the keys and values in a dictionary with another set of key value pairs. It adds keys and values that aren't there and overwrites the ones that are.
first = dict(a=1,b=2,c=3)
second = {}
second.update(first)
second # {"a":1, "b":2, "c":3}The syntax for a dictionary comprehension is similar to a list comprehension.
{___:___for___in___}It will iterate over keys by default. Use .items() to iterate over keys and values.
numbers = dict(first=1, second=2, third=3)
squared_numbers = {key: value ** 2 for key, value in numbers.items()}
squared_numbers # {"first": 1, "second": 4, "third": 9}You can use a dictionary comprehension to create a dictionary from another data structure.
{num: num ** 2 for num in [1, 2, 3, 4, 5]}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
num_list = [1, 2, 3, 4]
{num:("even" if num % 2 == 0 else "odd") for num in Num_list}
# {1: "odd", 2: "even", 3: "odd", 4: "even"}A tuple is an ordered collection or grouping of items, and it is immutable. It's an unchanging way of storing ordered data.
numbers = (1, 2, 3, 4)-
Tuples are faster than lists.
-
Tuples can make your code safer.
-
Tuples can be used as valid keys in a dictionary.
Calling .items() on a dictionary will return a list of tuples.
You can create a tuple with () of the tuple function.
You can access a tuple's values using [].
first_tuple = (1, 2, 3, 3, 3)
first_tuple[1] # 2
second_tuple = tuple(5, 1, 2)
second_tuple[0] # 5You can use a tuple as a key in a dictionary (where you couldn't use a list).
locations = {
(35.6895, 39.6917): "Tokyo Office",
(40.7128, 74.0060): "New York Office"
}You can use a for loop on a tuple, just like a list.
names = ("Colt", "Blue", "Rusty")
for name in names:
print(name)The count method returns the number of times a value is in a tuple.
x = (1, 2, 3, 3, 3)
x.count(3) # 3The index method retuns the first index at which a value is found in a tuple.
x = (1, 2, 3, 3, 3)
t.index(1) # 0
t.index(3) # 2Sets are like formal mathematical sets.
Sets are a collection of unordered data that do not have duplicate values.
You can't access items in a set by index.
You can create a set using {} or the set function.
first_set = set({1, 2, 2, 3, 4, 5, 5}) # {1, 2, 3, 4, 5}
second_set = {1, 4, 5}
4 in first_set # TrueConverting a list to a set can be useful if you want to distill it down to unique values or check how many unique values it has.
cities = ["Los Angeles", "London", "Florence", "London", "San Francisco", "Tokyo", "San Fransisco"]
list(set(cities)) # ["Los Angeles", "London", "Florence", "San Francisco", "Tokyo"]add adds an item to a set. If the element is already in the set, the set doesn't change.
s = set([1, 2, 3])
s.add(4) # {1, 2, 3, 4}remove removes a value from a set. It returns a KeyError if the value is not found. discard does the same thing but doesn't cayuse a KeyError.
set1 = {1, 2, 3, 4, 5, 6}
set1.remove(3)
set1 # {1, 2, 4, 5, 6}
set1.discard(3)copy creates a copy of a set.
s = set([1, 2, 3])
another_set = s.copy()
another_set # {1, 2, 3}
another_set is s # Falseclear removes all the contens of a set.
s = set([1, 2, 3])
s.clear()
s # set()Sets have several mathematical methods. For example, intersection, symmetric_difference, union.
math_students = {"Matthew", "Helen", "Prashant", "James", "Aparna"}
biology_students = {"Jane", "Matthew", "Charlotte", "Mesult", "Oliver", "James"}
math_students | biology_students # union
# {"Charlotte", "Prashant", "Jane", "James", "Oliver", "Mesult", "Helen", "Aparna", "Matthew"}
math_students & biology_students # intersection
# {"James", "Matthew"}Set comprehension can be useful when converting other data types to a set.
The syntax for a set comprehension is similar to a dictionary comprehension but you don't include both a key and value.
{x**2 for x in range(10)}
# {0, 1, 64, 4, 36, 9, 16, 49, 81, 25}For example, this function checks if a string contains all 5 vowels.
def are_all_vowels_in_string(string):
return len({char for char in string if char in "aeiou"}) == 5Functions are packages of code that you can call at any point. They can accept an input and return an output.
Functions help your code to stay dry and logically organized. They are also useful for abstracting away code.
The syntax is:
def name_of_function():
# block of reusable codeThe return keyword exits the function, outputs whatever is placed after the return keyword, and pops the function off the call stack.
Parameters are the values passed to a function. You can call them anything but try to give them descriptive names.
def square(num):
return num * num
def print_full_name(first_name, last_name):
return(f"Your full name is {first_name} and {last_name}")- Parameters refer to the variable in a method definition.
- When a method is called, the arguments are the data you pass into the method's parameters.
- Parameters are the variables in the declaration of a function.
- Arguments are the actual value of the variables that get passed to the function.
def sing_happy_birthday(name):
# Print song lyrics with name
sing_happy_birthday("Nicholas")Be careful with indentation or you may return too soon.
def sum_odd_numbers(numbers):
total = 0
for num in numbers:
if num % 2 == 0:
total += num
return num
print(sum_odd_numbers([1, 2, 3, 4, 5, 6, 7])) # 1
def sum_odd_numbers(numbers):
total = 0
for num in numbers:
if num % 2 == 0:
total += num
return num
print(sum_odd_numbers([1, 2, 3, 4, 5, 6, 7])) # 16You can set default parameters to be used if none are passed in.
They allow you to be more defensive and avoid errors with incorrect parameters.
def exponent(num, power=2):
return num ** power
exponent(2,3) # 8
exponent(7) # 49Default parameters can be anything - functions, lists, dictionaries, strings, Booleans.
def add(a,b):
return a + b
def subtract(a,b):
return a - b
def math(a,b, fn=add):
return fn(a,b)
math(2,2) # 4
math(2,2, subtract) # 0Keyword arguments let you specify which argument corresponds to which parameter if you know that name of the parameters. Order doesn't matter.
This makes functions more flexible and becomes important when passing a dictionary to a function.
def full_name(first, last):
return f"Your name is {first} {last}"
full_name(first="Colt", last="Steele") # Your name is Colt Steele
full_name(last="Steele", first="Colt") # Your name is Colt SteeleWhen you define a function and use an = you are setting a default parameter.
When you invoke a function and use an = you are making a keyword argument.
Scope governs where variables can be accessed.
Variables created in functions are scoped to that function.
def say_hello():
instructor = "Colt"
return f"Hello {instructor}"
say_hello() # Hello Colt
print(instructor) # NameErrorWhen you reference a global variable inside a function, the function assumes there is a local variable scoped to that function and will throw an error if you try to mutate the variable.
The global keyword lets you reference variables that were originally assigned in the global scope.
total = 0
def increment():
total += 1
return total
def decrement():
global total
total -= 1
return total
increment() # Error - local variable referenced before assignment
decrement() # -1The nonlocal keyword lets you modify a parent's variable in a child (nested) function.
def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner()When writing complex function, Python uses """ """ to document what the function does.
def say_hello():
"""A simple function that returns the string hello"""
return "Hello"
say_hello.__doc__ # 'A simple function that returns the string hello'*args is a special operator you can pass to a function as a parameter. It gathers all remaining arguments as a tuple.
As long as it starts with * it can be given other names, for example *nums, *details.
def sum_all_nums(*args):
total = 0
for num in args:
total += num
return total
sum_all_nums(4,6,9,4,10) # 33**kwargs (keyword args) is another special operator you can pass to a function. It gathers any remaining keyword arguments as a dictionary.
As long as it starts with ** you can give it other names, **extra_stuff.
def fav_colors(**kwargs):
print(kwargs)
fav_colors(colt="purple", ruby="red", ethel="teal")
# {"colt": "purple", "ruby": "red", "ethel": "teal"}In function declarations, use the following ordering:
- parameters
- *args
- default parameters
- **kwargs
You can use * as an argument to a function to unpack values from lists or tuples.
def sum_all_values(*args):
total = 0
for num in args:
total += num
return total
nums = [1, 2, 3, 4, 5, 6]
sum_all_values(nums) # Error
sum_all_values(*nums) # 21You can use ** to unpack a dictionary into keyword arguments.
def display_names(first, second):
print(f"{first} says hello to {second}")
names = {"first":"Colt", "second": "Rusty"}
display_names(names) # Error
display_names(**names) # "Colt says hello to Rusty"You can use a return statement to return multiple values from a function. These will be implicitly converted into a tuple. Write the return values in a comma separated list in the order you want them returned.
def get_coords():
x = 25.5
y = 48.2
return x,y
x, y = get_coords()Lambdas are short anonymous functions intended to be used one time.
lambda parameters: body of functionLambdas can be stored in variables but are more commonly passed into functions as a parameter.
def square(num): return num * num
square2 = lambda num: num * num
square(9) # 81
square2(9) # 81
scores = [20, 95, 45, 85, 90, 15, 5, 100, 10]
high_scores = filter(lambda:score: score >= 90, scores)
button = tk.Button(frame,
text="CLICK ME",
fg="red",
command=lambda: print("Hello")
)map is a standard function that accepts at least two arguments, a function and an iterable.
An iterable is something that can be iterated over (lists, strings, dictionaries, sets, tuples).
map runs a lambda for each value in the iterable and returns a map object which can be converted into another data structure.
lst = [1, 2, 3, 4]
doubles = list(map(lambda X: x*2, lst)) # [2, 4, 6, 8]
names = [
{"first": "Colt", "last":"Steele"},
{"first": "Rusty", "last":"Steele"},
{"first": "Blue", "last":"Steele"},
]
first_names = list(map(lambda x: x["first"], names))
# ["Colt", "Rusty", "Blue"]filter allows you to take a collection and filter out desired items.
filter calls a lambda for each value in the iterable. It returns a filter object which can be converted into another iterable. The object contains only the values that returned True to the lambda.
lst = [1, 2, 3, 4]
evens = list(filter(lambda num: num % 2 == 0, lst)) # [2, 4]You can combine filter and map:
names = ["Laurie", "Colt", "Rusty"]
list(map(lambda name: f"Your instructor is {name}",
filter(lambda value: len(value) < 5, name)))
# ["Your instructor is Colt"]If you can use list comprehension to do the same thing, you probably should.
[f"Your instructor is {name}" for name in name if len(name) < 5]all returns True if all elements of the iterable passed in are truthy (or if the iterable is empty).
all([0, 1, 2, 3]) # False
all([char for char in "eio" if char in "aeiou"]) # True
all([num for num in [4, 2, 10, 6, 8] if num % 2 == 0]) # Trueany returns True if any element of the iterable is truthy. If the iterable is empty, it returns False.
nums = [2, 60, 26, 18, 21]
any([num % 2 == 1 for num in nums]) # TrueIf all you're doing is iterating once and you don't need to store and use the generated results, you can use a generator expression in place of a list comprehension. It's more lightweight in terms of memory.
import sys
list_comp = sys.getsizeof([x * 10 for x in range(1000)]) # 9024 bytes
gen_exp = sys.getsizeof(x * 10 for x in range(1000)) # 88 bytessorted returns a new sorted list from the items in an iterable. It doesn't change the original iterable.
more_nums = [6, 1, 8, 2]
sorted(more_nums) # [1, 2, 6, 8]You can sort in descending order by including reverse=True
more_nums = [6, 1, 8, 2]
sorted(more_nums, reverse=True) # [8 , 6, 2, 1]You can use sorted with dictionaries by using key to specify what to sort by.
users = [
{"name": "Katie", "tweets": ["I make ceramics", "We put another rover on Mars"]},
{"name": "Colt", "tweets": ["I love cats"]},
{"name": "Rusty", "tweets": [], "id": 1234, "pro": True },
]
sorted(users, key=lambda user: user["name"])
sorted(users, key=lambda user: len(user["tweets"]))max returns the largest item in an iterable or the largest of two or more arguments. You can specify how things are sorted using key.
max(3, 67, 99) # 99
max("hello world") # "w"
names = ["Arya", "Samson", "Dora", "Tim", "Ollivander"]
max(names) # Tim - alphabetical
max(names, key=lambda n: len(n)) # Ollivander - lengthmin returns the smallest item in an iterable or the smallest of two or more arguments.
min((36, 21, 7)) # 7
song = {
{"title": "Happy Birthday", "playcount": 1},
{"title": "I Will Survive", "playcount": 6},
{"title": "YMCA", "playcount": 99},
{"title": "Toxic", "playcount": 31},
}
min(songs, key=lambda song: song["playcount"])reversed returns a reversed iterator (works with other structures than lists). You'd use it if you were iterating over something in reverse.
for x in reversed(range(0, 10)):
print(x)len returns the length (number of items) of an object. The argument may be a sequence (such as a string, tuple, list or range) or a collection (such as a dictionary or set).
len("asdasdas") # 8
len({}) # 0Each object has a built in method __len__() which is called when you use len.
abs returns the absolute value of a number. The argument may be an integer or float.
abs(-5) # 5
abs(5.44) # 5.44sum takes an iterable and an optional start (by default 0). It returns the sum of the start and the items of the iterable from left to right.
sum([1,2,3]) # 6
sum([1,2,3], 10) # 16round returns a number rounded to ndigits after the decimal point. If ndigits is ommitted or is None, it returns the nearest integer to the input.
round(10.2) # 10
round(1.212121, 2) # 1.21zip makes an iterator that aggregates elements from each of the provided iterables.
It returns an iterator of tuples, where the i-th tuple contains the i-th element from each of the provided iterables.
The iterator stops when the shortest input iterable is exhausted.
first_zip = zip([1, 2, 3], [4, 5, 6])
list(first_zip) # [(1, 4), (2, 5), (3, 6)]
dict(first_zip) # {1: 4, 2: 5, 3: 6}You can use * to unpack a list of tuples before zipping them. This becomes useful when working with more complex data structures.
five_by_two = [(0, 1), (1, 2), (2, 3), (3, 4), (4,5)]
list(zip(*five_by_two))
# [(0, 1, 2, 3, 4), (1, 2, 3, 4, 5)]midterms = [88, 91, 78]
finals = [98, 89, 73]
students = ["kate", "ang", "dan"]
# zip + dictionary comprehension
final_grades = {t[0]: max(t[1], t[2]) for t in zip(students, midterms, finals)}
# {"kate": 98, "ang": 91, "dan":78}
# zip + lambda
grades = dict(
zip(
students,
map(
lambda pair: max(pair),
zip(midterms, finals)
)
)
)
# {"kate": 98, "ang": 91, "dan":78}SyntaxError occurs when Python encounters something it can't parse. Usually due to typos.
NameError occurs when a variable hasn't been assigned yet.
TypeError occurs when an operation or function is applied to the wrong type.
IndexError occurs when you try to access an element in a list using an invalid index (one outside it's range).
ValueError occurs when a built-in operation or function receives an argument that has the right type but an inappropriate value, for example int("abcd").
KeyError occurs when a dictionary does not have a specific key.
AttributeError occurs when a variable does not have an attribute, for example [1, 2, 3].greet().
You can deliberately raise specific errors using the raise keyword.
def colorize(text, color):
colors = ("cyan", "yellow", "blue", "green", "magenta")
if type(text) != str:
raise TypeError("text must be instance of str")
if type(color) != str:
raise TypeError("color must be instance of str")
if color not in colors:
raise ValueError("color is invalid color")
print(f"Printed {text} in {color}")In Python you're strongly encouraged to use try/except blocks to catch and handle exceptions.
You don't want to catch all errors with a blank except. You need to correctly identify what went wrong.
def getKey(d, key):
try:
return d[key]
except KeyError:
return Nonetry will attempt to do something. If there's a problem except will run. If there's not a problem else runs. No matter what, finally runs.
while True:
try:
num = int(input("please enter a number: "))
except ValueError:
print("That's not a number")
else:
print("Good job, you entered a number")
break
finally:
print("Runs no mattter what")
print("The rest of the logic runs")If you want the error itself, you can access it using as
def divide(a,b):
try:
result = a/b
except ZeroDivisionError:
print("Please don't divide by zero")
except TypeError as err:
print("arguments must be ints or floats")
print(err)
else:
print(f"{a} divided by {b} is {result}")pdb is a module that lets you set breakpoints in your code, check values, and step through your code.
import pdb; pdb.set_trace()import pdb
first = "First"
second = "Second"
pdb.set_trace()
result = first + second
third = "Third"
result += third
print(result)Common pdb commands include:
- l - list
- n - next line
- p - print
- c - continue, finishes debugging
Modules help keep your files small and reuse code across files.
import random
random.choice(["apple", "banana", "cherry", "durian"])
random.shuffle(["apple", "banana", "cherry", "durian"])You can alias a module when importing it.
import random as omg_so_random
omg_so_random.choice(["apple", "banana", "cherry", "durian"])The keyword from lets you import only the parts of a module that you need.
from random import randint, choice
from random import * # makes everything available in the namespaceYou can import code from one of your Python files into another file. This is helpful for keeping things organized and reusable.
# file1
def fn():
return "Do something"
def other_fn():
return "Do the other thing"
# file2
import file1
file1.fn()
file1.other_fn()External modules can be downloaded using pip, Python's package manager. These modules can be found via PyPi, the Python package index.
python3 -m pip install NAME_OF_PACKAGEfrom pyfiglet import figlet_format
from termcolor import colored
def print_art(msg, color):
valid_colors = ("red", "green", "yellow", "blue", "magenta", "cyan")
if color not in valid_colors:
color = "magenta"
ascii_art = figlet_format(msg)
colored_ascii = colored(ascii_art, color=color)
print(colored_ascii)
msg = input("What would you like to print? ")
color = input("What color? ")
print_art(msg, color)A Python virtual environment is a folder structure that lets you run an isolated, lightweight Python installation with its own packages. It reproduces the folder structure that a standard Python installation creates while keeping all installed packages isolated to that environment.
The main reasons to use virtual environments is to avoid dependency conflicts and avoid global namespace pollution.
Different projects might require different versions of external libraries. If everything is installed globally, then you cannot work with two versions of the same library.
Linux and MacOS come with a version of Python pre-installed that the OS uses for internal tasks. Globally installed packages can interfere with this. Also, OS updates can overwrite globally installed packages.
There are multiple ways to create a virtual environment but the venv module comes installed with Python.
To create a virtual environment in a directory, run:
python3 -m venv <VENV_NAME>.venv is a popular name for virtual environments.
You need to activate the virtual environment to be able to use it.
source .venv/bin/activate
(.venv) $You can then install packages into just the virtual environment.
(.venv) $ python3 -m pip install <PACKAGE_NAME>When you are finished working in a virtual environment you deactivate it.
(.venv) $ deactivate
$If you have a project the locally uses a virtual environment, anyone else trying to work on it on their machine would need to replicate the virtual environment.
You don't commit your virtual environment to version control or ship it with your project.
You can create a list of all the dependencies for a project, and their versions, by creating a requirements.txt file.
python3 -m pip freeze > requirements.txtTo install all the dependencies from a project that was created in a virtual environment, activate the virtual environment that you want to work in and run:
python3 -m pip install -r requirements.txtWhen working with virtual environments, if you get a warning that pip is out of date
(.venv) $ python -m pip install --upgrade pipThe Python Standard Library is a collection of modules that comes with every Python installation.
The standard library includes modules for working with different data types, the filesystem, the operating system, networking, GUIs, etc.
import random
def main():
number = random.randInt(1,1000)
numbers = [1,2,3,4,5,6,7]
random.shuffle(numbers)
return
if __name__ == "__main__":
main()The autopep8 package will automatically format your Python code to conform to the pep8 styleguide.
You can set autopep8 to be more or less aggressive in its formatting.
autopep8 --in-place --aggressive --aggressive <filename>
For example, -aor --aggressive enables non-whitespace changes; multiple -a result in more aggressive changes
When run, every Python file has a __name__ variable.
If the file is the main file being run, its value is main. Otherwise, its value is the file name, for example if the file is being imported into another file.
When you use import, Python:
- Tries to find the module (if it fails it throws an error), that is a
.pyfile with the same name as__name__. - Runs the code inside the module being imported.
You can tell Python to not run certain lines on import and only run them when the file is run directly as a script by checking if __name__ is __main__:
def main:
# do something
if __name__ == "__main__":
# this code will only run
# if the file is the main file being run
main()When you enter a url into your browser, the folowing steps happen:
- DNS lookup turns the url into an IP address
- Your computer makes a REQUEST to a server
- The server processes the REQUEST
- The server issues a RESPONSE.
This is the Request/Response cycle.
DNS lookup connects domain names and IP addresses through a DNS server.
HTTP headers are sent with both requests and responses. They provide additional information about the request or response.
Accept- Acceptable content-types for response (for example, html, json, xml)Cache-Control- Specify caching behaviourUser-Agent- Contains information about the software used to make the requestAccess-Control-Allow-Origin- Specify domains that can make requestsAllowed- HTTP verbs that are allowed in requests
Response Status Codes
- 2xx - Success
- 3xx - Redirect
- 4xx - Client Error (your fault)
- 5xx - Server Error (not your fault)
GET Requests
- Used for retrieving data
- Data passed in a query string
- Should have no side-effects
- Can be cached
- Can be bookmarked
POST Requests
- Used for writing data
- Data passed in request body
- Can have side-effects
- Not cached
- Can't be bookmarked
An API can be thought of as the version of a website intended for computers rather than humans.
APIs allow you to get data from another application without needing to understand how that application works.
Often, they can send the data back in different formats, such as json or xml.
The requests module lets you make http requests using Python. It's useful for web scraping/crawling and grabbing data from APIs.
You can use request headers to specify the format you want to receive data in from an API.
import requests
url = "https://icanhazdadjoke.com/"
response = requests.get(url, headers={"Accept": "application/json"})
data = response.json()
print(data["joke"])When you make a json request, it comes back as one giant string. You need to use the json method to turn it into a Python dictionary.
A query string is a way to pass data to a server as part of a GET request: http://www.example.com/?key1=value1&key2=value2
The params are the kay value pairs you include in it.
import requests
url = "https://icanhazdadjoke.com/search"
response = requests.get(
url,
headers={"Accept": "application/json"},
params={"term": "cat", "limit": 1}
)
data = response.json()
print(data["results"])You can make POST requests by providing the url and data in the form of JSON.
def save_post(data):
  url = "https://jsonplaceholder.typicode.com/posts"
  response = requests.post(url, json=data)
  parsed_data = response.json()
  return parsed_dataAn API may not raise an error, for example, if a resource doesn't exist. The requests module provides a function, raisefor_status, that will manually raise an error if the response's status code indicates an error (4xx, 5xx).
requests also provide error types that can be checked for, such as requests.exceptions.HTTPError for status codes in the 400 and 500 range, or requests.exceptions.RequestException if the request timed out.
def get_resource():
  url = "https://jsonplaceholder.typicode.com/xyz"
  try:
    response = requests.get(url)
    response.raise_for_status()
    parsed_data = response.json()
    return parsed_data
  except requests.exceptions.HTTPError as error:
    print(f"There was a HTTP error: {error}")
  except requests.exceptions.RequestException as error:
    print(f"There was an error: {error}")OOP is a method of programming that attempts to model some process or thing in the world as a class or object.
class - a blueprint for objects. Classes can contain methods (functions) and attributes (similar to keys in a dict)
instance - objects that are constructed from a class blueprint that contain their class's methods and properties.
With OOP, your goal is to encapsulate your code into logical, hierarchical groupings using classes so that you can reason about your code at a higher level.
Say you want to model a game of poker. You could have the following entities:
- Game
- Player
- Card
- Deck
- Hand
- Chip
- Bet
A hyptehtical Deck class might have the following attributes and methods:
- _cards (private list attribute)
- _max_cards (private list attribute)
- shuffle (public method)
- deal_card (public method)
- deal_hand (public method)
- count (public method)
Some things don't need to be exposed to the outside world, they can be accessed through methods.
Unlike with Java, in Python there isn't a true distinction between public and private but good code follows the convention of designating some things as private.
encapsulation - the grouping of public and private atrributes and methods ito a programmatic class, making abstraction possible.
abstraction - exposing only "relevant" data in a class interface, hiding private attributes and methods (aka the "inner workings") from users.
Classes in Python can have a special __init__ method, which gets called every time you create (instantiate) an instance of the class. This initializes the data in the instance.
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
car1 = Vehicle("Ford", "Fiesta", "2012")
car2 = Vehicle("Honda", "Civic", "2014")Creating an object that is an instance of a class is called instantiating a class.
The self keyword refers to the specific instance of the class you're working with. self must always be the first parameter to __init__ and to any methods and properties you define on class instances.
class Person:
def __init__(self):
self.name = "Tony"
self._secret = "hi"
self.__msg = "Turtles all the way down"
p = Person()Dunder methods, like __init__, are built into Python. You generally don't define your own.
Private methods and attributes, not intended for use outside of a class, are conventionally written with a leading underscore self._secret.
When you put double underscores before the name of a method or attribute, Python "mangles" the name of that attribute, making it particular to that class. For example, self.__msg becomes _Person__msg
Classes are not just storage places for information, they also have functionality.
self is necessary for all methods, even if you aren't using it.
class User:
def __init__(self, first, last, age):
self.first = first
self.last = last
self.age = age
def full_name(self):
return f"{self.first} {self.last}"
def initials(self):
return f"{self.first[0]}.{self.last[0]}."
def likes(self, thing):
return f"{self.first} likes {thing}"
def is_senior(self):
return self.age >= 65
def birthday(self):
self.age += 1
return f"Happy {self.age} birthday, {self.first}"
user1 = User("Joe", "Woods", 35)
user2 = User("Bianca", "Lopez", 38)If an instance attribute is defined on each individual instance of a class, a class attribute is defined once and lives on the class itself. It is shared by all instances of the class and by the class itself.
class User:
active_users = 0 # <-- class attribute
def __init__(self, first, last, age):
self.first = first
self.last = last
self.age = age
User.active_users += 1
def logout(self):
User.active_users -= 1
return f"{self.first} has logged out"
user1 = User("Joe", "Woods", 35)
user2 = User("Bianca", "Lopez", 38)
print(User.active_users) # 2class Pet:
allowed = ["cat", "dog", "fish", "rodent"]
def __init__(self, name, species):
if species not in Pet.allowed:
raise ValueError(f"You can't have a {species} as a pet!")
self.name = name
self.species = species
def set_species(self, species):
if species not in Pet.allowed:
raise ValueError(f"You can't have a {species} as a pet!")
self.species = speciesYou can refer to a class attribute with Pet.allowed or self.allowed, as long as you're just accessing and not updating the value. Using the class name is more conventional.
Class methods are methods (with the @classmethod decorator) that are not concerned with instances, but with the class itself (often passed into the class method as cls).
One use for a class methid is if you need to pass in data in one format and convert it to another before __init__ is called.
class User:
# ...
@classmethod
def display_active_users(cls):
return f"There are currently {cls.active_users} active users"
@classmethod
def from_string(cls, data_str):
first, last, age = data_str.split(",")
return cls(first, last, int(age))
User.display_active_users()
tom = User.from_string("Tom,Jones,79")Static methods are similar to class level methods but are bound to the class itself rather than instances of the class.
A static method can be called without an object for that class.
Static methods cannot modify the state of an object as they are not bound to it.
Static methods can be denoted using the @staticmethod decorator.
Static methods don't need self to be passed as the first argument.
class User:
...
@staticmethod
def validate_email(email):
# validation logic
User.validate_email("leo@example.com")The __repr__ method is one of several ways to provide a nicer string representation of a class. When you print a class or turn a class into a string, __repr__ is called behind the scenes.
__repr__ is what allows a list, for example, to be printed as [].
class Human:
def __init__(self, name="somebody"):
self.name = name
def __repr__(self):
return self.name
fella = Human()
print(fella) # "somebody"
colt = Human(name="Colt Steele")
print(f"{colt} is totally rad (probably)")
# "Colt Steele is totally rad (probably)"There are also several other dunders to return classes in string formats (__str__, __format__) and choosing one is a bit complicated.
A key feature of OOP is the ability to define a class which inherits from another class (a "base" or "parent" class).
In Python, inheritance works by passing the parent class as an argument to the definition of a child class.
class Animal:
...
def make_sound(self, sound):
print(sound)
class Cat(Animal):
pass
tivaldo = Cat()
tivaldo.make_sound("meow") # "meow" Without having private methods or attributes, Python relies on the following conventions. Properties let you approximate private methods and attributes.
class Human:
def __init__(self, first, last, age):
self.first = first
self.last = last
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value >= 0:
self._age = value
else:
raise ValueError("age can't be a negative")When initialising a child class, super lets you reduce duplication. super will refer to the parent class and automatically pass in self as the first argument.
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def __repr__(self):
return f"{self.name} is a {self.species}"
def make_sound(self, sound):
print(sound)
class Cat(Animal):
def __init__(self, name, breed, toy):
super().__init__(name, species="Cat")
self.breed = breed
self.toy = toy
def play(self):
print(f"{self.name} plays with {self.toy}")
blue = Cat("Blue", "Scottish Fold", "String")
blue.play() # Blue plays with StringA class can inherit from more than one parent class. It's not super common and a bit controversial.
class Aquatic:
def __init__(self, name):
self.name = name
def swim(self):
return f"{self.name} is swimming"
def greet(self):
return f"I am {self.name} of the sea"
class Ambulatory:
def __init__(self, name):
self.name = name
def walk(self):
return f"{self.name} is walking"
def greet(self):
return f"I am {self.name} of the land"
class Penguin(Aquatic, Ambulatory):
def __init__(self, name):
super().__init__(name=name)
jaws = Aquatic("Jaws")
lassie = Ambulatory("Lassie")
captain_cook = Penguin("Captain Cook")Penguin inherits from both Aquatic and Ambulatory, so instances of Penguin can call both the walk and swim methods.
Penguin calls Aquatic.greet() instead of Ambulatory.greet().
Whenever you create a class, Python automatically sets a Method Resolution Order (MRO) for that class, which is the order in which Python will look for methods on instances of that class.
You can programmatically reference the MRO three ways:
__mro__attribute on the class- use the
mro()method on the class - use the built-in
help(cls)method
A key proinciple of OOP is the idea of polymorphism - an object can take on many forms.
Two practical applications of polymorphism are:
- The same class method works in a similar way for different classes.
A common implementation of this is to have a method in a parent class that is overridden by a subclass (method overriding).
Each subclass will have a different implementation of the method.
Cat.speak()
Dog.speak()
Human.speak()- The same operation works for different kinds of objects.
Python classes have special, "magic", methods, that are dunders. These methods have special names that give instructions to Python for how to deal with objects.
len(sample_list)
len(sample_tuple)
len(sample_string)In the operation 8 + 2 Python knows what to do because the + operator is shorthand for a special method __add__() that gets called on the first operand.
If the first (left) operand is an instance of int, __add__() does mathematical addition. If it's a string, it does concatenation.
You can delcare special methods on your own classes to mimic the behaviour of built-in objects.
class Human:
def __init__(self, height):
self.height = height # in cms
def __len__(self):
return self.height
colt = Human(180)
len(colt) # 180A common use case for special methods is to make classes "look pretty" in strings.
By default classes are ugly when printed.
class Human:
pass
colt = Human()
print(colt) # <__main__.Human at 0x1062b8400>The __repr__ method is one of several ways to provide a nicer string representation.
class Human:
def __init__(self, name="somebody"):
self.name = name
def __repr__(self):
return self.name
yer_man = Human()
print(yer_man) # "somebody"An Iterator is an object that can be iterated upon. It returns data, one element at a time, when next() is called on it.
An Iterable is an object whcih will return an Iterator when iter() is called on it.
"hello" is not an iterator, but it is an iterable.
iter("hello") returns an iterator.
When you call a for loop on a string, for example, the for loop calls iter() on it.
When next() is called on an iterator, the iterator returns the next item. It keeps doing so until it raises a StopIteration error.
# Custom for loop
def my_for(iterable, func):
iterator = iter(iterable)
while True:
try:
item = next(iterator)
except StopIteration:
break
else:
func(item)
def square(x):
print(x*x)
my_for("hello", print)
my_for([1, 2, 3], sum)
my_for([1, 2, 3], square)class Counter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
return self
def __next__(self):
if self.current < self.high:
num = self.current
self.current += 1
return num
raise StopIteration
for n in Counter(50,55):
print(n)Generators are a subset of iterators.
They can be created with generator functions using the yield function or with generator expressions.
| Functions | Generator Functions |
|---|---|
uses return |
uses yield |
| returns once | can yield multiple times |
| when invoked, returns the return value | When invoked, returns a generator |
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1def current_beat():
nums = (1,2,3,4)
i = 0
while True:
if i >= len(nums): i = 0
yield nums[i]
i += 1Generators can be much more memory efficient than using functions with lists, especially when dealing with large sequences.
def fib_list(max):
nums = []
a, b = 0, 1
while len(nums) < max:
nums.append(b)
a, b = b, a+b
return nums
fib_list(1000000) # 503 MB
def fib_gen(max):
x = 0
y = 1
count = 0
while count < max:
x, y = y, x + y
yield x
count += 1
for n in fib_gen(1000000):
print(n) # 6.7 MBGenerator expressions are a concise syntax for creating generators, comparable to list comprehensions for lists.
# Generator function
def nums():
for num in range(1,10):
yield num
# Generator Expression
g = (num for num in range(1,10))Generator expressions can be much more time efficient than list comprehensions.
sum(n for n in range(100000000)) # 6.75 seconds
sum([n for n in range(100000000)]) # 10.47 secondsHigher order functions can accept other functions as arguments and return functions.
They can make your code more reusable and modular. A higher order function can be reused without having specific logic tied to it.
There are several higher order functions built into Python, such as map and filter.
moves = ["punch", "kick", "block", "dodge"]
def power_move(move):
return f"{move.upper()}!!!"
power_moves = map(power_move, moves)
scores = [20, 95, 45, 85, 90, 15, 55, 100, 10]
def is_high_score(score):
return score >= 90
high_scores = filter(is_high_score, scores)Decorators are functions that wrap other functions, changing their behaviour.
They have their own syntax, using @.
def be_polite(fn):
def wrapper():
print("What a pleasure to meet you!")
fn()
print("Have a great day")
return wrapper
@be_polite
def greet():
print("My name is Matt")
greet() # same as greet = be_polite(greet)Real-world use cases for decorators might include:
@require_auth- check user auth before function conditionally runs@validate_input- validate function arguments before function runs@preprocess- modify function arguments to be in a specific format
Decorated functions can accept arguments using *args and **kwargs.
def my_decorator(fn):
def wrapper(*args, **kwargs):
# do some stuff with fn(*args, **kwargs)
pass
return wrapperdef shout(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs*).upper()
return wrapper
@shout
def greet(name):
return f"Hi, I'm {name}"
@shout
def order(main, side):
return f"Hi, I'd like the {main} with a side of {side}, please"By default, decorated functions don't maintain their metadata. But Python has a module functools with a function wraps which preserves a function's metadata when it is decorated.
from functools import wraps
def my_decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
# do some stuff with fn(*args, **kwargs)
pass
return wrapperfrom functools import wraps
def log_function_data(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
"""I AM A WRAPPER FUNCTION"""
print(f"You are about to call {fn.__name__}")
print(f"Here's the documentation: {fn.__doc__}")
return fn(*args, **kwargs)
return wrapper
@log_function_data
def add(x,y):
"""Adds two numbers together"""
return x + yDecorators can be used to enforce restrictions on arguments.
from functools import wraps
def ensure_no_kwargs(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if kwargs
raise ValueError("No kwargs allowed")
return fn(*args, **kwargs)
return wrapper
@ensure_no_kwargs
def greet(name):
print(f"Hi there, {name}")
greet("Ali") # "Hi there, Ali"
greet(name="Ali") # ValueErrorDecorators can accept arguments and ensure that an argument is a specific value.
from functools import wraps
def ensure_first_arg_is(val):
def inner(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if args and args[0] != val:
return f"First arg needs to be {val}"
return fn(*args, **kwargs)
return wrapper
return inner
@ensure_first_arg_is(10)
def add_to_ten(num1, num2):
return num1 + num2
print(add_to_ten(10,22)) # 12
print(add_to_ten(1,2)) # "First arg needs to be 10"You can also use a decorator to enforce types.
from functools import wraps
def enforce(*types):
def decorator(f):
def new_func(*args, **kwargs):
# convert args into something mutable
newargs = []
for (a,t) in zip (args, types):
newargs.append(t(a))
return f(*newargs, **kwargs)
return new_func
return decorator
@enforce(str, int)
def repeat_msg(msg, times):
for time in range(times):
print(msg)
repeat_msg("hello", "3")A closure is a function that remembers values from its enclosing scope even after the enclosing scope has finished execution.
def move_factory(character_name):
  uppercase_name = character_name.upper()
  def print_move(move_name):
    # uppercase_name is available from enclosing scope
    print(f"{uppercase_name} performs {move_name}")
  return print_move
def main():
  ryu_move = move_factory("Ryu")
  ryu_move("Hadouken")
if __name__ == "__main__":
  main()Writing tests reduces bugs in existing code, ensures that bugs stay fixed, and ensures that new features don't break old ones, also passing tests is satisfying.
With Test Driven Development:
- development begins by writing tests
- once tests are written, write code to make the tests pass
- once tests pass, a feature is considered complete.
A mantra of TDD is "red, green, refactor".
- Write a test that fails
- Write the minimal amount of code to make the test pass
- Clean up the code, while ensuring that tests still pass
You can make simple assertion statements with the assert keyword. assert accepts an expression. It returns None if the expression is truthy and it raises an AssertionError if the expression is falsy. assert accepts an optional error message as a second argument.
def add_positive_numbers(x, y):
assert x > 0 and y > 0, "Both numbers must be positive!"
return x + yIf a Python file is run with the -O flag, assertions will not be evaluated.
There are better options available for testing.
You can write tests for functions inside the docstring. But you have to write the code as if it's in a REPL. The syntax is quite finnicky, it clutters up your functions, tests can be brittle, and it lacks many of the features of larger testing tools.
def add(a, b):
"""
>>> add(2,3)
5
>>> add(100,200)
300
"""
return a + bTo run doctests, use:
python3 -m doctest -v name_of_file.py
Unit testing is the idea of testing the smallest parts of an application in isolation.
Good candidates for unit testing are individual classes, modules, or functions.
Bad candidates for unit testing would be an entire application or dependencies across several classes or modules.
Python comes with a built-in module called unittest You write unit tests as classes that inherit from unittest.TestCase. This inheritance gives you access to many assertion helpers that let you test the behaviour of your functions.
You run tests by calling unittest.main().
import unittest
from activities import eat, nap
class ActivityTest(unittest.TestCase):
def test_eat_healthy(self):
"""eat should have a positive message for healthy eating"""
self.assertEqual(
eat("broccoli", is_healthy=True),
"I'm eating broccoli, because my body is a temple"
)
def test_eat_unhealthy(self):
"""eat should indicate you've given up for unhealthy eating"""
self.assertEqual(
eat("pizza", is_healthy=False),
"I'm eating pizza, because YOLO"
)
def test_short_nap(self):
"""short naps should be refreshing"""
self.assertEqual(
nap(1),
"I'm feeling refreshed after my 1 hour nap"
)
def test_long_nap(self):
"""long naps should be discouraging"""
self.assertEqual(
nap(3),
"Ugh, I overslept. I didn't mean to nap for 3 hours"
)
if __name__ == "__main__":
unittest.main()You can add comments to tests to make them more informative.
To see comments, run
python3 name_of_test_file.py -v
self.assertEqual(x,y)self.assertNotEqual(x,y)self.assertTrue(x)self.assertFalse(x)self.assertIsNone(x)self.assertIsNotNone(x)
When testing for errors, self.assertRaises lets you test for a specific type of error.
class SomeTests(unittest.TestCase):
def testing_for_errors(self):
"""testing for an error"""
with self.assertRaises(IndexError):
l = [1,2,3]
l[100]For larger applications, you may want similar application state before running tests.
setUp runs before each test method.
tearDown runs after each test method.
Common use cases include: adding/removing data from a test database, creating instances of a class.
class SomeTests(unittest.TestCase):
def setUp(self):
# do setup here
pass
def test_first(self):
# setUp runs before
# tearDown runs after
pass
def test_second(self):
# setUp runs before
# tearDown runs after
pass
def tearDown(self):
# do teardown here
passYou can read a file by passing the file to the open function.
open returns a file object.
You can then read the file object with the read method.
file = open("story.txt")
file.read()Python reads files using a cursor (like the one you see when typing). After a file is read, the cursor is at the end of the file. If you call read again on the same file, you'll get an empty string.
You use the seek method to move the cursor.
file.seek(0) will put the cursor at the start of the file.
The readline method will read the specified line and stop at the newline character.
The readlines method returns a list of lines.
You can close a file with the close method.
You can check if a file is closed using the closed attribute.
Once closed, a file can't be read again until you reopen it. Always close files, it frees up system resources.
The with statement gives you an alternative syntax for file I/O. Once it's run, the file is automatically closed. Behind the scenes, __enter__() and __exit__() are called.
file = open("story.txt")
file.read()
file.close()
file.closed # Truewith open("story.txt") as file:
file.read()
file.closed # TrueYou can also use open to write to a file.
You need to specify the w flag as the second argument.
You don't need to have an existing file to write to. Writing to a non-existing file will create it.
with open("haiku.txt", "w") as file:
file.write("Writing files is great\n")
file.write("Here is another line of text\n")
file.write("Closing now, goodbye!\n")- r - Read a file (no writing) - this is the default
- w - Write to a file (previous content is removed)
- a - Append to the end of a file (previous content is not removed)
- r+ - Read and write to a file (writes based on cursor - starts at beginning, only works with existing files)
You can pass a relative path to open. That is relative to where the Python script is being called, not where the script file is located.
file = open("sample.txt", "r")Sometimes you will need an absolute path but you don't want to hardcode this or the script won't work on other machines.
The pathlib module from the Python standard library lets you construct paths.
You create a new Path instance and pass it __file__ (the full absolute path, including the filename, as a string).
You can then access properties on this path object, such as parent (the absolute path to the file's parent directory).
You can add directories to the path using / and make a directory using the mkdir method.
path.write_text() lets you write text to a file without manually opening it first. path.read_text() similarly lets you read a file without manually opening it.
from pathlib import Path
def create_path():
script_dir = Path(__file__).parent
path = script_dir / "characters"
# create any parent folders, if needed
# if directory already exists, don't throw error
path.mkdir(parents=True, exist_ok=True)
path = path / "zelda.txt"
file = path.open("w") # equivalent to file = open(path, "w")
file.write("Ganon")
content = file.read()
print(content)
file.close()
path.write_text("Epona")
return
def main():
create_path()
if __name__ == "__main__":
main()Working with files can cause errors, for example if you try to read a file that doesn't exist you'll get a FileNotFoundError.
For this reason you'll want to wrap file operations with a try block. Remember to include a catchall error case.
from pathlib import Path
def open_file():
  path = Path(__file__).parent
  path = path / "does" / "not" / "exist"
  try:
    file = path.open("r")
    contents = file.read()
    print(contents)
    file.close()
  except FileNotFoundError:
    print(f"{path} does not exist")
  except Exception as e:
    print(f"Unexpected error: {e}")
def main():
  open_file()
if __name__ == "__main__":
  main()Context managers are a tool to automatically handle the setup and teardown phases when dealing with external resources.
For example, they allow you to open a file, work with the file, and then the file will automatically close when the work is done.
The with statement creates a runtime context that lets you run a group of statements under the control of a context manager.
from pathlib import Path
def open_file():
  path = Path(__file__).parent / "characters.txt"
  characters = ["Mario", "Luigi", "Peach", "Yoshi", "Bowser"]
  # context manager - auto closes
  with path.open("w") as file:
    for character in characters:
      file.write(character + "\n")
def main():
  open_file()
if __name__ == "__main__":
  main()Python supports working with JSON through the built-in json module, which lets you convert common data types between Python and JSON.
| Python | JSON |
|---|---|
| dict | object |
| list | array |
| tuple | array |
| str | string |
| int | number |
| float | number |
| True | true |
| False | false |
| None | null |
json.dump lets you serialize a Python dictionary into a JSON string. |
json.load lets you deserialize JSON into Python.
from pathlib import Path
import json
path = Path(__file__).parent / "characters.json"
characters = {
  "characters": [
    {"name": "Mario", "age": 25},
    {"name": "Luigi", "age": 22},
    {"name": "Peach", "age": 26},
    {"name": "Bowser", "age": 35},
  ]
}
def write_json():
  with path.open("w") as file:
  # data to write, target file, indent
    json.dump(characters, file, indent=2)
  return
def read_json():
  with path.open("r") as file:
    data = json.load(file)
  return data
def main():
  write_json()
  data = read_json()
  print(data)
if __name__ == "__main__":
  main()CSV files are a common file format for tabular data. The first row contains the headers, setting up what the data is.
You can read CSV files like any other file, but don't do this.
Python has a built-in CSV module to read/write CSVs more easily.
reader lets you iterate over CSV rows as lists.
from csv import reader
with open "fighters.csv" as file:
csv_reader = reader(file)
next(csv_reader) # skip headers row
for fighter in csv_reader:
print(fighter) # each row is a list
print(f"{fighter[0]} is from {fighter[1]}")DictReader lets you iterate over CSV rows as OrderedDicts.
from csv import DictReader
with open "fighters.csv" as file:
csv_reader = DictReader(file)
for row in csv_reader:
print(row) # each row is an OrderedDict
print(row["Name"])Readers accept a delimiter kwarg in case your data isn't separated by commas.
from csv import reader
with open "example.csv" as file:
csv_reader = reader(file, delimiter="|")
for row in csv_reader:
print(row) # each row is a listwriter - creates a writer object for writing to CSV
writerow - method on a writer to write a row to the CSV
from csv import writer
with open("fighters.csv", "w") as file:
csv_writer = writer(file)
csv_writer.writerow(["Character", "Move"])
csv_writer.writerow(["Ryu", "Hadouken"])You can use nested with statements.
from csv import reader, writer
with open("fighters.csv") as file:
csv_reader = reader(file)
with open("screaming_fighters.csv", "w") as file:
csv_writer = writer(file)
for fighter in csv_reader:
csv_writer.writerow([s.upper() for s in fighter])You can also use writerows to write a list of values to multiple rows.
drinks = ["Guinness", "Jameson", "Cavan Cola"]
writer.writerow(["Drink Name"])
writer.writerows(drinks)DictWriter creates a writer object for writing using dictionaries.
fieldnames - kwarg for the DictWriter specifying headers
writeheader - method on writer to write the header row
writerow - method on writer to write a row based on a dictionary
from csv import DictWriter
with open("example.csv", "w") as file:
headers = ["Character", "Move"]
csv_writer = DictWriter(file, fieldnames=headers)
csv_writer.writeheader()
csv_writer.writerow({
"Character": "Ryu",
"Move": "Hadouken"
})pickle is a module that serializes data, for storing in a file or database, and which you can deserialize and access later.
The data is not human readable while stored.
import pickle
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def __repr__(self):
return f"{self.name} is a {self.species}"
def make_sound(self, sound):
print(f"this animal says {sound}")
class Cat(Animal):
def __init__(self, name, breed, toy):
super().__init__(name, species="Cat") # Call init on parent class
self.breed = breed
self.toy = toy
def play(self):
print(f"{self.name} plays with {self.toy}")
blue = Cat("Blue", "Scottish Fold", "String")
# To pickle an object:
with open("pets.pickle", "wb") as file:
pickle.dump(blue, file)
# To unpickle something:
with open("pets.pickle", "rb") as file:
zombie_blue = pickle.load(file)
print(zombie_blue)
print(zombie_blue.play())JSON, while created to send JavaScript code over the web, is now used across multiple languages.
Python has a built-in json module.
json.dumps formats a Python object as a string of JSON.
j = json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
# ["foo", {"bar": ['baz', null, 1.0, 2]}]jsonpickle is a library that lets you pickle using JSON, which does take up more space but is human-readable.
import jsonpickle
class Cat:
def __init__(self, name, breed):
self.name = name
self.breed = breed
c = Cat("Charles", "Tabby")
# pickle as JSON
with open("cat.json", "w") as file:
frozen = jsonpickle.encode(c)
# {"py/object": "__main__.Cat", "breed": "Tabby", "name": "Charles"}
file.write(frozen)
# unpickle from JSON
with open("cat.json", "r") as file:
contents = file.read()
unfrozen = jsonpickle.decode(contents)Web scraping involves programmatically getting data from a web page.
There are three steps: download, extract data, PROFIT!
Why scrape?
- There's data on a site that you want to store or anaylze.
- You can't get it by other means (an API)
- You want to programmatically grab the data (instead of lots of manual copy/pasting)
Is scraping... ok?
- Some websites don't want people scraping them
- Best practice is to consult the
robots.txtfile - If making many requests, time them out
- If you're too aggressive, your IP can be blocked
BeautifulSoup lets you navigate through HTML with Python.
BeautifulSoup does not download HTML, you need to use the request module.
BeautifulSoup(html_string, "html.parser") - parses HTML
Once parsed, there are several ways to navigate:
- By tag name
- Using
find- returns one matching tag - Using
find_all- returns a list of matching tags - Using CSS selectors with
select- returns a list of matching tags
from bs4 import BeautifulSoup
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>First HTML Page</title>
</head>
<body>
<div id="first">
<h3 data-example="yes">hi</h3>
<p>more text.</p>
</div>
<ol>
<li class="special">This list item is special.</li>
<li class="special">This list item is also special.</li>
<li>This list item is not special.</li>
</ol>
<div data-example="yes">bye</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
print(soup.body.div) # first div
a = soup.find(id="first")
b = soup.select("#first")[0] # returns a list
c = soup.find_all(class_="special")
d = soup.select(".special")
e = soup.find_all(attrs={"data-example": "yes"})
f = soup.select("[data-example]")
print(d)get_text- method to access the inner text in an element.name- method to retrieve the tag's nameattrs- method to get a dictionary of attributes- using square brackets and the name of an attribute -
soup.find("h3")["data-example"]
Navigating with BeautifulSoup involves finding elements relevant to an element.
Via tags:
parent/parentscontentsnext_sibling/next_siblings
Via searching:
find_parent/find_parentsfind_next_sibling/find_next_siblingsfind_previous_sibling/find_previous_siblings
Using tags includes newline characters, so you need to remeber to skip over them.
contents, gives you a list with a tag's contents, including newline characters.
soup.body.contents[1].next_sibling.next_sibling
The searching methods, such as find_next_sibling, skip newline characters.
Scrapy is a Python framework for web crawling and web scraping, used to crawl websites and extract structured data from their pages.
It operates like BeautifulSoup, parsing HTML for data, even though it has a different syntax.
import scrapy
class BookSpider(scrapy.Spider):
name = "bookspider"
start_urls = ["https://books.toscrape.com"]
def parse(self, response):
for article in response.css("article.product_pod"):
yield {
"price": article.css(".price_color::text").extract_first(),
"title": article.css("h3 > a::attr(title)").extract_first()
}
next = response.css(".next > a::attr(href)").extract_first()
if next:
yield response.follow(next, self.parse)To run Scrapy, call it and pass it the Python file to run and the destination file to save the data to.
scrapy runspider -o books.csv book_scraper.py
Regular expressions (regexes) are a way of describing patterns within strings.
For example, if you want to check that an email is in a valid email format, you need to check that it:
- starts with one or more letters, numbers, +, _,-,.
- a single @ sign
- one or more letters, numbers, or -
- a single dot
- ends with one or more letters, numbers, -, or .
Instead of writing several conditional statements you can use a regex.
(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)
Potential use cases include:
- credit card number validating
- phone number validating
- advanced find/replace in text
- formatting text/output
- syntax highlighting
| Character | Matches |
|---|---|
| \d | digit 0-9 |
| \w | letter, digit or underscore |
| \s | whitespace character |
| \D | not a digit |
| \W | not a letter, digit or underscore |
| \S | not a whitespace character |
| . | any character except line break |
| Quantifier | Matches |
|---|---|
| + | one or more |
| {3} | exactly x times. {3} - 3 times |
| {3,5} | Three to five times |
| {4,} | Four or more times |
| * | zero or more times |
| ? | once or none (optional |
You can create a group of acceptable values using [], for example [a-zA-Z0-9].
Inside [] ^ means not, rather than starts with. So [^@$] means any character that is not @ or $.
| Character | Sets boundary |
|---|---|
| ^ | start of string or line |
| $ | end of string or line |
| \b | word boundary (space or newline) |
| works as logical or. For example \(\d{3}\)|\d{3} matches three digits with or without parens.
() groups characters. (\(\d{3}\)|\d{3}) \d{3} \d{4}
Python has a built-in re module for working with regexes.
With re you create a regex object and then use its methods.
import re
pattern = re.compile(r'\d{3} \d{3}-\d{4}')
res = pattern.search("Call me at 415 555-4242")A regex to parse a url looks something like this. You can use parens to group parts of the regex into capture groups.
import re
url_regex = re.compile(r'(https?)://(www\.[A-za-z-]{2,256}\.[a-z]{2,6})([-a-zA-Z0-9@:%_\+.~#?&//=]*)')
match = url_regex.search("https://www.my-website.com/bio?data=blah&dog=yes")
print(f"Protocol: {match.group(1)}")
print(f"Domain: {match.group(2)}")
print(f"Everything Else: {match.group(3)}")
print(match.groups())
print(match.group())If you call group, or group(0) on the match you will get the entire url.
group(1), group(2), group(3) will give you the first, second and third capture group.
groups() will give you a tuple of the capture groups.
In a regex, you can give groups names to represent them.
name_regex = re.compile(r'^(Mr\.|Mrs\.|Ms\.|Mdme\.) (?P<first>[A-Za-z]+) (?P<last>[A-Za-z]+)$')Flags let you modify how regexes work.
For example, re.IGNORECASE re.I lets you do case-insensitive searches.
The verbose flag (re.VERBOSE or re.X) lets you put a regex on multiple lines and comment it.
You can combine flags using |.
email_regex = re.compile(r"^([a-z0-9_\.-])@([0-9a-z\.-]+)\.([a-z\.]{2,6})$")
verbose_email_regex = re.compile(r"""
^([a-z0-9_\.-]) # username
@ # single @
([0-9a-z\.-]+) # email provider
\. # single .
([a-z\.]{2,6})$ # top level domain
""", re.VERBOSE | re.IGNORECASE)Find and replace is a common use-case for regular expressions.
The sub method takes a pattern and what you want to replce that pattern with.
import re
def censor(input):
pattern = re.compile(r'\bfrack\w*\b', re.IGNORECASE)
return pattern.sub("CENSORED", input)You can select capture groups using the syntax \g<> and the group number.
import re
text = "Last night Mrs. Peach and Ms. Daisy murdered Mr. Bowser"
pattern = re.compile(r'(Mr\.|Mrs\.|Ms\.) ([a-z])[a-z]+', re.I)
result = pattern.sub("\g<1> \g<2>", text)
# Last night Mrs. P and Ms. D murdered Mr. B.SQL is popular database choice with Python.
With SQLite, each value is one of five datatypes.
-
NULL - a NULL value.
-
INTEGER - a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value.
-
REAL - a floating point value, stored as an 8-byte IEEE floating point number.
-
TEXT - a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16LE).
-
BLOB - a blob of data, stored exactly as it was input.
By default Sqlite conects to a transient in-memory database. To save it to a file, open/create a file with .open FILENAME or sqlite3 FILENAME.
SQLite follows the same syntax as other SQL databases.
CREATE TABLE table_name (column_name datatype);
INSERT INTO table_name (column_names) VALUES (values);
You can run a SQL file using .read file_name.sql inside Sqlite.
SELECT * FROM table_name;
SELECT * from table_name WHERE column_name IS value;
SELECT * from table_name WHERE column_name IS NOT value AND column_name2 IS NOT value2;
SELECT * FROM table_name WHERE column_name LIKE %value%;
Python comes with a built-in library to communicate with SQLite.
First you create a connection to the database (if the database doesn't exist it'll be created).
Create a cursor (a temporary workspace for SQL commands).
Use the cursor to execute your SQL command, such as creating or inserting into a table.
Commit the changes.
Finally, close the connection.
import sqlite3
# Create connection
conn = sqlite3.connect("my_friends.db")
# Create cursor object
c = conn.cursor()
# Execute SQL
c.execute("CREATE TABLE friends (first_name TEXT, last_name TEXT, closeness INTEGER);")
# Commit changes
conn.commit()
# Close connection
conn.close()You can create a query before you execute it.
insert_query = "INSERT INTO friends VALUES ('Merry', 'Lewis', 7);"
c.execute(insert_query)You can interpolate variables into your query using ? and a tuple. This handles quotes and sanitizing the data.
data = ("Mary", "Reed", 9)
query = "INSERT INTO friends VALUES (?,?,?);"
c.execute(query, data)You can carry out bulk inserts using executemany.
people = [
("Roald", "Amundsen", 5),
("Rosa", "Parks", 9),
("Henry", "Hudson", 5),
("Neil", "Armstrong", 7),
("Daniel", "Bruhl", 3)
]
c.executemany("INSERT INTO friends VALUES (?,?,?)", people)If you want to do something with each item along with inserting it, use a loop.
for person in people:
c.execute("INSERT INTO friends VALUES (?,?,?)", person)
print("Inserting now")When you select with Python, you can iterate over the cursor object, use c.fetchall() to get back a list containing the results or c.fetchone() to get just the first result.
import sqlite3
conn = sqlite3.connect("my_friends.db")
c = conn.cursor()
c.execute("SELECT * FROM friends WHERE closeness > 5 ORDER BY closeness;")
c.fetchall()
conn.commit()
conn.close()If you don't sanitize your inputs, a user could write SQL into their input and manipulate the database.
In the example below, if a user entered ' OR 1=1-- the single quote would close the quote, 1=1 would always evaluate to true and -- would comment out any remaining characters.
import sqlite3
conn = sqlite3.connect("users.db")
c = conn.cursor()
user_name = input("Please enter your username...")
password = input("Please enter your password...")
# Bad idea
# query = f"SELECT * FROM users WHERE username='{user_name}' AND password='{password}' OR 1=1 --'"
# Better idea
query = "SELECT * FROM users WHERE username=? AND password=?"
c.execute(query, (user_name, password))
result = c.fetchone()
if(result):
print("Welcome back")
else:
print("Failed login")
conn.commit()
conn.close()Python 3.10 added structural pattern matching using match statements.
This lets you:
- extract information from complex data types
- have conditional branching based on the structure of the data
- apply specific actions based on different forms of data
A match statement takes an expression and compares its value to patterns in one or more case blocks. Each pattern is checked from top to bottom.
You can optionally include a wildcard case (commonly _) as the last case. If there is no exact match, the wildcard will be used as the matching case. If there is no match and no wildcard there will be a no-op and nothing will happen.
You can combine patterns using | ("or").
def http_error(status):
match status:
case 400:
return "Bad request"
case 401 | 403:
return "Not allowed"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"Patterns can match a literal value or extract and bind a variable
# point is an (x,y) tuple
match point:
case (0,0):
print("Origin")
case (0,y):
print(f"Y={y}")
case (x,0):
print(f"X={x}")
case (x,y):
print(f"X={x}, Y={y}")
case other:
raise ValueError("Not a point)You can match a class by using the class name and constructor argument list as a pattern. This pattern lets you capture class attributes as variables.
class Point:
x: int
y: int
def location(point):
match point:
case Point(x=0, y=0):
print("Origin is the point's location")
case Point(x=0, y=y):
print(f"Y={y} and the point is on the y-axis")
case Point(x=x, y=0):
print(f"X={x} and the point is on the x-axis")
case Point():
print("The point is located somewhere else on the plane")
case _:
print("Not a point")A wildcard can be used in more complex patterns where you don't care what one or more of the values is.
match test_variable:
case ("warning", code, 40):
print("A warning was received")
case ("error", code, _):
print(f"An error with code [{code}] occurred")You can add an "if" clause to a pattern as a guard. If the guard is false, the match goes on to the next case block. Value capturing happens before the guard is evaluated.
match point:
case Point(x, y) if x == y:
print(f"The point is located on the diagonal Y=X at {x}")
case Point(x, y):
print("Point is not on the diagonal")