├── 30_imports_in_python ├── libs │ ├── __init__.py │ └── mylib.py ├── mymodule.py └── code.py ├── 31_relative_imports_python ├── __init__.py ├── libs │ ├── __init__.py │ ├── operations │ │ └── operator.py │ └── mylib.py ├── code.py └── mymodule.py ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── 04_first_python_app └── code.py ├── README.md ├── .gitignore ├── Pipfile ├── 09_the_in_keyword └── code.py ├── 03_getting_user_input ├── code.py └── slide.html ├── 07_booleans └── code.py ├── 24_functions_vs_methods └── README.md ├── 17_default_parameter_values └── code.py ├── 01_variables └── code.py ├── 08_if_statements └── code.py ├── 02_string_formatting ├── code.py └── slide.html ├── 05_lists_tuples_sets └── code.py ├── 39_dataclasses └── code.py ├── 12_list_comprehension └── code.py ├── 20_dictionary_comprehensions └── code.py ├── 36_the_at_syntax_for_decorators └── code.py ├── 18_functions_returning_values └── code.py ├── 06_advanced_set_operations └── code.py ├── 13_dictionaries └── code.py ├── 28_class_composition └── code.py ├── 10_if_with_in └── code.py ├── 19_lambda_functions └── code.py ├── 21_unpacking_arguments └── code.py ├── 25_str_and_repr └── code.py ├── 15_functions └── code.py ├── 16_function_arguments_parameters └── code.py ├── 14_destructuring_variables └── code.py ├── 23_object_oriented_programming └── code.py ├── 27_class_inheritance └── code.py ├── 41_mutable_default_parameters └── code.py ├── 22_unpacking_keyword_arguments └── code.py ├── 26_classmethod_staticmethod └── code.py ├── 11_loops └── code.py ├── 37_decorating_functions_with_parameters └── code.py ├── 33_custom_error_classes └── code.py ├── 29_type_hinting └── code.py ├── 38_decorators_with_parameters └── code.py ├── 40_mutability └── code.py ├── CONTRIBUTING.md ├── 34_first_class_functions └── code.py ├── 32_errors_in_python └── code.py ├── 35_simple_decorators_python └── code.py └── Pipfile.lock /30_imports_in_python/libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /31_relative_imports_python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /31_relative_imports_python/libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /30_imports_in_python/libs/mylib.py: -------------------------------------------------------------------------------- 1 | print("mylib.py:", __name__) 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "python.pythonPath": "/Users/jslvtr/.local/share/virtualenvs/course-template-JRUUZj0H/bin/python" 4 | } -------------------------------------------------------------------------------- /31_relative_imports_python/libs/operations/operator.py: -------------------------------------------------------------------------------- 1 | print("operator.py", __name__) 2 | 3 | # -- can do parent imports in file with parent package -- 4 | 5 | from ..mylib import * 6 | -------------------------------------------------------------------------------- /04_first_python_app/code.py: -------------------------------------------------------------------------------- 1 | user_age = input("Enter your age: ") 2 | age_number = int(user_age) 3 | 4 | months = age_number * 12 5 | print(f"{age_number} is equal to {months} months.") 6 | -------------------------------------------------------------------------------- /31_relative_imports_python/code.py: -------------------------------------------------------------------------------- 1 | import mymodule 2 | 3 | print("code.py:", __name__) 4 | 5 | # -- can't do relative imports from the __main__ file: 6 | 7 | # from . import mymodule 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Refresher 2 | 3 | This repository contains our Python Refresher or Reference code, ordered by lecture. 4 | 5 | Feel free to bookmark or download this code to come back to it at a later date! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Python_refresher_notes 2 | *.cmproj 3 | *.tscproj 4 | *.pyc 5 | __pycache__ 6 | *.mp4 7 | *.mkv 8 | *.hfp 9 | tempCodeRunnerFile.py 10 | .vscode/ 11 | .idea/ 12 | videos 13 | exports 14 | captions 15 | -------------------------------------------------------------------------------- /31_relative_imports_python/libs/mylib.py: -------------------------------------------------------------------------------- 1 | from libs.operations import operator 2 | 3 | print("mylib.py:", __name__) 4 | 5 | # -- can do relative imports from file with parent package -- 6 | 7 | from .operations import operator 8 | -------------------------------------------------------------------------------- /30_imports_in_python/mymodule.py: -------------------------------------------------------------------------------- 1 | def divide(dividend, divisor): 2 | return dividend / divisor 3 | 4 | 5 | print("mymodule.py:", __name__) 6 | 7 | # -- importing from a folder -- 8 | 9 | import libs.mylib 10 | 11 | print("mymodule.py:", __name__) 12 | -------------------------------------------------------------------------------- /31_relative_imports_python/mymodule.py: -------------------------------------------------------------------------------- 1 | from libs import mylib 2 | 3 | print("mymodule.py:", __name__) 4 | 5 | # -- can't do relative imports from top-level file -- 6 | 7 | from .libs import mylib 8 | 9 | # -- parent imports -- 10 | 11 | import libs.operations.operator 12 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | black = "*" 8 | pre-commit = "*" 9 | 10 | [packages] 11 | 12 | [requires] 13 | python_version = "3.7" 14 | 15 | [pipenv] 16 | allow_prereleases = "true" 17 | -------------------------------------------------------------------------------- /09_the_in_keyword/code.py: -------------------------------------------------------------------------------- 1 | friends = ["Rolf", "Bob", "Jen"] 2 | print("Jen" in friends) 3 | 4 | # -- 5 | 6 | movies_watched = {"The Matrix", "Green Book", "Her"} 7 | user_movie = input("Enter something you've watched recently: ") 8 | 9 | print(user_movie in movies_watched) 10 | 11 | # The `in` keyword works in most sequences like lists, tuples, and sets. 12 | -------------------------------------------------------------------------------- /03_getting_user_input/code.py: -------------------------------------------------------------------------------- 1 | name = input("Enter your name: ") 2 | print(name) 3 | 4 | # -- Mathematics on user input -- 5 | 6 | size_input = input("How big is your house (in square feet): ") 7 | square_feet = int(size_input) 8 | square_metres = square_feet / 10.8 # Make sure this is correct 9 | print(f"{square_feet} square feet is {square_metres} square metres.") 10 | -------------------------------------------------------------------------------- /07_booleans/code.py: -------------------------------------------------------------------------------- 1 | # -- Comparisons -- 2 | 3 | print(5 == 5) # True 4 | print(5 > 5) # False 5 | print(10 != 10) # False 6 | 7 | # Comparisons: ==, !=, >, <, >=, <= 8 | 9 | # -- is -- 10 | # Python also has the `is` keyword. It's a confusing keyword for now, so I don't recommend using it. 11 | 12 | friends = ["Rolf", "Bob"] 13 | abroad = ["Rolf", "Bob"] 14 | 15 | print(friends == abroad) # True 16 | print(friends is abroad) # False 17 | -------------------------------------------------------------------------------- /24_functions_vs_methods/README.md: -------------------------------------------------------------------------------- 1 | # Functions vs. Methods 2 | 3 | A function that lives inside a class is called a method. 4 | 5 | So, methods are functions, but not all functions are methods. 6 | 7 | ## Function 8 | 9 | ```python 10 | def average(sequence): 11 | return sum(sequence) / len(sequence) 12 | ``` 13 | 14 | ## Method 15 | 16 | ```python 17 | class Student: 18 | def __init__(self): # method 19 | self.name = "Rolf" 20 | self.grades = (79, 90, 95, 99) 21 | 22 | def average(self): # method 23 | return sum(self.grades) / len(self.grades) 24 | ``` -------------------------------------------------------------------------------- /17_default_parameter_values/code.py: -------------------------------------------------------------------------------- 1 | def add(x, y=3): 2 | print(x + y) 3 | 4 | 5 | add(5) # 8 6 | add(5, 8) # 13 7 | add(y=3) # Error, missing x 8 | 9 | # -- Order of default parameters -- 10 | 11 | # def add(x=5, y): # Not OK, default parameters must go after non-default 12 | # print(x + y) 13 | 14 | # -- Usually don't use variables as default value -- 15 | 16 | default_y = 3 17 | 18 | 19 | def add(x, y=default_y): 20 | sum = x + y 21 | print(sum) 22 | 23 | 24 | add(2) # 5 25 | 26 | default_y = 4 27 | print(default_y) # 4 28 | 29 | add(2) # 5, even though we re-defined default_y 30 | -------------------------------------------------------------------------------- /01_variables/code.py: -------------------------------------------------------------------------------- 1 | # -- Numbers and Floats -- 2 | 3 | x = 15 4 | price = 9.99 5 | 6 | discount = 0.2 7 | 8 | result = price * (1 - discount) 9 | 10 | print(result) 11 | 12 | # -- Strings -- 13 | 14 | name = "Rolf" 15 | name = "Rolf" 16 | 17 | print(name) 18 | print(name * 2) 19 | 20 | # -- Changing variables -- 21 | # Variables are names for values. 22 | 23 | a = 25 24 | b = a 25 | 26 | # Here we've given the value '25' the names 'a' and 'b'. 27 | 28 | print(a) 29 | print(b) 30 | 31 | b = 17 32 | 33 | # Here we've given the value '17' the name 'b'. The name 'a' is still a name for '25'! 34 | 35 | print(a) 36 | print(b) 37 | -------------------------------------------------------------------------------- /08_if_statements/code.py: -------------------------------------------------------------------------------- 1 | day_of_week = input("What day of the week is it today? ") 2 | 3 | if day_of_week == "Monday": 4 | print("Have a great start to your week!") 5 | elif day_of_week == "Friday": 6 | print("It's ok to finish a bit early!") 7 | else: 8 | print("Full speed ahead!") 9 | 10 | # -- Problem: user not entering what we expect -- 11 | 12 | day_of_week = input("What day of the week is it today? ").lower() 13 | 14 | if day_of_week == "monday": 15 | print("Have a great start to your week!") 16 | elif day_of_week == "friday": 17 | print("It's ok to finish a bit early!") 18 | else: 19 | print("Full speed ahead!") 20 | -------------------------------------------------------------------------------- /02_string_formatting/code.py: -------------------------------------------------------------------------------- 1 | name = "Bob" 2 | greeting = "Hello, Bob" 3 | 4 | print(greeting) 5 | 6 | name = "Rolf" 7 | 8 | print(greeting) 9 | 10 | greeting = f"Hello, {name}" 11 | print(greeting) 12 | 13 | # -- 14 | 15 | name = "Anne" 16 | print( 17 | greeting 18 | ) # This still prints "Hello, Rolf" because `greeting` was calculated earlier. 19 | print( 20 | f"Hello, {name}" 21 | ) # This is correct, since it uses `name` at the current point in time. 22 | 23 | # -- Using .format() -- 24 | 25 | # We can define template strings and then replace parts of it with another value, instead of doing it directly in the string. 26 | 27 | greeting = "Hello, {}" 28 | with_name = greeting.format("Rolf") 29 | print(with_name) 30 | 31 | longer_phrase = "Hello, {}. Today is {}." 32 | formatted = longer_phrase.format("Rolf", "Monday") 33 | print(formatted) 34 | -------------------------------------------------------------------------------- /05_lists_tuples_sets/code.py: -------------------------------------------------------------------------------- 1 | l = ["Bob", "Rolf", "Anne"] 2 | t = ("Bob", "Rolf", "Anne") 3 | s = {"Bob", "Rolf", "Anne"} 4 | 5 | # Access individual items in lists and tuples using the index. 6 | 7 | print(l[0]) 8 | print(t[0]) 9 | # print(s[0]) # This gives an error because sets are unordered, so accessing element 0 of something without order doesn't make sense. 10 | 11 | # Modify individual items in lists using the index. 12 | 13 | l[0] = "Smith" 14 | # t[0] = "Smith" # This gives an error because tuples are "immutable". 15 | 16 | print(l) 17 | print(t) 18 | 19 | # Add to a list by using `.append` 20 | 21 | l.append("Jen") 22 | print(l) 23 | # Tuples cannot be appended to because they are immutable. 24 | 25 | # Add to sets by using `.add` 26 | 27 | s.add("Jen") 28 | print(s) 29 | 30 | # Sets can't have the same element twice. 31 | 32 | s.add("Bob") 33 | print(s) 34 | -------------------------------------------------------------------------------- /39_dataclasses/code.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class Student: 5 | def __init__(self, name: str, grades: List[int] = None): 6 | self.name = name 7 | self.grades = grades or [] 8 | 9 | def take_exam(self, result): 10 | self.grades.append(result) 11 | 12 | 13 | bob = Student("Bob") 14 | bob.take_exam(90) 15 | print(bob) 16 | 17 | # -- as dataclass -- 18 | 19 | from dataclasses import dataclass, field 20 | 21 | 22 | @dataclass 23 | class Student: 24 | name: str 25 | grades: List[int] = field( 26 | default_factory=list 27 | ) # if we want to run a function, use default_factory and it will run the function to generate the default 28 | 29 | def take_exam(self, result): 30 | self.grades.append(result) 31 | 32 | 33 | bob = Student("Bob") 34 | 35 | bob.take_exam(90) 36 | print(bob.grades) 37 | print(bob) 38 | -------------------------------------------------------------------------------- /12_list_comprehension/code.py: -------------------------------------------------------------------------------- 1 | numbers = [1, 3, 5] 2 | squares = [x * 2 for x in numbers] 3 | 4 | # -- Dealing with strings -- 5 | 6 | friends = ["Rolf", "Sam", "Samantha", "Saurabh", "Jen"] 7 | starts_s = [] 8 | 9 | for friend in friends: 10 | if friend.startswith("S"): 11 | starts_s.append(friend) 12 | 13 | print(starts_s) 14 | 15 | 16 | # -- Can make a new list of friends whose name starts with S -- 17 | 18 | friends = ["Rolf", "Sam", "Samantha", "Saurabh", "Jen"] 19 | starts_s = [friend for friend in friends if friend.startswith("S")] 20 | 21 | print(starts_s) 22 | 23 | # -- List comprehension creates a _new_ list -- 24 | 25 | friends = ["Sam", "Samantha", "Saurabh"] 26 | starts_s = [friend for friend in friends if friend.startswith("S")] # same as above 27 | 28 | print(friends) 29 | print(starts_s) 30 | print(friends is starts_s) 31 | print("friends: ", id(friends), " starts_s: ", id(starts_s)) 32 | -------------------------------------------------------------------------------- /20_dictionary_comprehensions/code.py: -------------------------------------------------------------------------------- 1 | users = [ 2 | (0, "Bob", "password"), 3 | (1, "Rolf", "bob123"), 4 | (2, "Jose", "longp4assword"), 5 | (3, "username", "1234"), 6 | ] 7 | 8 | username_mapping = {user[1]: user for user in users} 9 | userid_mapping = {user[0]: user for user in users} 10 | 11 | print(username_mapping) 12 | 13 | print(username_mapping["Bob"]) # (0, "Bob", "password") 14 | 15 | # -- Can be useful to log in for example -- 16 | 17 | username_input = input("Enter your username: ") 18 | password_input = input("Enter your password: ") 19 | 20 | _, username, password = username_mapping[username_input] 21 | 22 | if password_input == password: 23 | print("Your details are correct!") 24 | else: 25 | print("Your details are incorrect.") 26 | 27 | # If we didn't use the mapping, the code would require us to loop over all users. 28 | # Shown on the side, pause the video if you want to read it thoroughly. 29 | -------------------------------------------------------------------------------- /36_the_at_syntax_for_decorators/code.py: -------------------------------------------------------------------------------- 1 | user = {"username": "jose", "access_level": "guest"} 2 | 3 | 4 | def make_secure(func): 5 | def secure_function(): 6 | if user["access_level"] == "admin": 7 | return func() 8 | else: 9 | return f"No admin permissions for {user['username']}." 10 | 11 | return secure_function 12 | 13 | 14 | @make_secure 15 | def get_admin_password(): 16 | return "1234" 17 | 18 | 19 | # -- keeping function name and docstring -- 20 | import functools 21 | 22 | 23 | user = {"username": "jose", "access_level": "guest"} 24 | 25 | 26 | def make_secure(func): 27 | @functools.wraps(func) 28 | def secure_function(): 29 | if user["access_level"] == "admin": 30 | return func() 31 | else: 32 | return f"No admin permissions for {user['username']}." 33 | 34 | return secure_function 35 | 36 | 37 | @make_secure 38 | def get_admin_password(): 39 | return "1234" 40 | -------------------------------------------------------------------------------- /30_imports_in_python/code.py: -------------------------------------------------------------------------------- 1 | # -- importing -- 2 | 3 | from mymodule import divide 4 | 5 | print(divide(10, 2)) 6 | 7 | # -- __name__ -- 8 | 9 | print(__name__) 10 | 11 | # -- importing with names -- 12 | 13 | import mymodule 14 | 15 | print("code.py: ", __name__) 16 | 17 | # How does Python know where `mymodule` is? 18 | # Answer, it looks at the paths in sys.path in order: 19 | 20 | import sys 21 | 22 | print(sys.path) 23 | 24 | # The first path is the folder containing the file that you ran. 25 | # The second path is the environment variable PYTHONPATH, if it is set. 26 | # We won't cover environment variables in depth here, but they are variables you can set in your operating system. It can help Python find things to import when the folder structure is ambiguous. 27 | 28 | # -- circular imports -- 29 | # Make mymodule.py import code.py as well. 30 | 31 | # -- importing from a folder -- 32 | 33 | import mymodule 34 | 35 | print("code.py: ", __name__) 36 | 37 | import sys 38 | 39 | print(sys.modules) 40 | -------------------------------------------------------------------------------- /18_functions_returning_values/code.py: -------------------------------------------------------------------------------- 1 | def add(x, y): 2 | print(x + y) 3 | 4 | 5 | add(5, 8) 6 | result = add(5, 8) 7 | print(result) # None 8 | 9 | # If we want to get something back from the function, it must return a value. 10 | # All functions return _something_. By default, it's None. 11 | 12 | # -- Returning values -- 13 | 14 | 15 | def add(x, y): 16 | return x + y 17 | 18 | 19 | add(1, 2) # Nothing printed out anymore. 20 | result = add(2, 3) 21 | print(result) # 5 22 | 23 | # -- Returning terminates the function -- 24 | 25 | 26 | def add(x, y): 27 | return 28 | print(x + y) 29 | return x + y 30 | 31 | 32 | result = add(5, 8) # Nothing printed out 33 | print(result) # None, as is the first return 34 | 35 | # -- Returning with conditionals -- 36 | 37 | 38 | def divide(dividend, divisor): 39 | if divisor != 0: 40 | return dividend / divisor 41 | else: 42 | return "You fool!" 43 | 44 | 45 | result = divide(15, 3) 46 | print(result) # 5 47 | 48 | another = divide(15, 0) 49 | print(another) # You fool! 50 | -------------------------------------------------------------------------------- /06_advanced_set_operations/code.py: -------------------------------------------------------------------------------- 1 | # -- Difference between two sets -- 2 | 3 | friends = {"Bob", "Rolf", "Anne"} 4 | abroad = {"Bob", "Anne"} 5 | 6 | # local_friends = ... 7 | # If there are 3 friends, and 2 are abroad, that means that 1 friend is local. 8 | # We can easily calculate which names are in `friends` but not in `abroad` by using `.difference` 9 | 10 | local = friends.difference(abroad) 11 | print(local) 12 | 13 | print(abroad.difference(friends)) # This returns an empty set 14 | 15 | # -- Union of two sets -- 16 | 17 | local = {"Rolf"} 18 | abroad = {"Bob", "Anne"} 19 | 20 | # friends = ... 21 | # If we have 1 local friend and 2 abroad friends, we could calculate the total friends by using `.union` 22 | 23 | friends = local.union(abroad) 24 | print(friends) 25 | 26 | # -- Intersection of two sets -- 27 | 28 | art = {"Bob", "Jen", "Rolf", "Charlie"} 29 | science = {"Bob", "Jen", "Adam", "Anne"} 30 | 31 | # Given these two sets of students, we can calculate those who do both art and science by using `.intersection` 32 | 33 | both = art.intersection(science) 34 | print(both) 35 | -------------------------------------------------------------------------------- /13_dictionaries/code.py: -------------------------------------------------------------------------------- 1 | friend_ages = {"Rolf": 24, "Adam": 30, "Anne": 27} 2 | 3 | friend_ages["Bob"] = 20 4 | 5 | print(friend_ages) # {'Rolf': 24, 'Adam': 30, 'Anne': 27, 'Bob': 20} 6 | print(friend_ages["Bob"]) 7 | 8 | # -- List of dictionaries -- 9 | 10 | friends = [ 11 | {"name": "Rolf Smith", "age": 24}, 12 | {"name": "Adam Wool", "age": 30}, 13 | {"name": "Anne Pun", "age": 27}, 14 | ] 15 | 16 | print(friends) 17 | 18 | # -- Iteration -- 19 | 20 | student_attendance = {"Rolf": 96, "Bob": 80, "Anne": 100} 21 | 22 | for student in student_attendance: 23 | print(f"{student}: {student_attendance[student]}") 24 | 25 | # Better 26 | 27 | for student, attendance in student_attendance.items(): 28 | print(f"{student}: {attendance}") 29 | 30 | # -- Using the `in` keyword -- 31 | 32 | if "Bob" in student_attendance: 33 | print(f"Bob: {student_attendance[student]}") 34 | else: 35 | print("Bob isn't a student in this class!") 36 | 37 | # -- Calculate an average with `.values()` -- 38 | 39 | attendace_values = student_attendance.values() 40 | print(sum(attendace_values) / len(attendace_values)) 41 | -------------------------------------------------------------------------------- /28_class_composition/code.py: -------------------------------------------------------------------------------- 1 | # Something I see a lot, but you SHOULDN'T DO 2 | 3 | 4 | class BookShelf: 5 | def __init__(self, quantity): 6 | self.quantity = quantity 7 | 8 | def __str__(self): 9 | return f"BookShelf with {self.quantity} books." 10 | 11 | 12 | shelf = BookShelf(300) 13 | 14 | 15 | class Book(BookShelf): 16 | def __init__(self, name, quantity): 17 | super().__init__(quantity) 18 | self.name = name 19 | 20 | 21 | # This makes no sense, because now you need to pass `quantity` to a single book: 22 | 23 | book = Book("Harry Potter", 120) 24 | print(book) # What? 25 | 26 | # -- Composition over inheritance here -- 27 | 28 | # Inheritance: "A Book is a BookShelf" 29 | # Composition: "A BookShelf has many Books" 30 | 31 | 32 | class BookShelf: 33 | def __init__(self, *books): 34 | self.books = books 35 | 36 | def __str__(self): 37 | return f"BookShelf with {len(self.books)} books." 38 | 39 | 40 | class Book: 41 | def __init__(self, name): 42 | self.name = name 43 | 44 | 45 | book = Book("Harry Potter") 46 | book2 = Book("Python 101") 47 | shelf = BookShelf(book, book2) 48 | print(shelf) 49 | -------------------------------------------------------------------------------- /10_if_with_in/code.py: -------------------------------------------------------------------------------- 1 | movies_watched = {"The Matrix", "Green Book", "Her"} 2 | user_movie = input("Enter something you've watched recently: ") 3 | 4 | if user_movie in movies_watched: 5 | print(f"I've watched {user_movie} too!") 6 | else: 7 | print("I haven't watched that yet.") 8 | 9 | # -- 10 | 11 | number = 7 12 | user_input = input("Enter 'y' if you would like to play: ") 13 | 14 | if user_input in ("y", "Y"): 15 | user_number = int(input("Guess our number: ")) 16 | if user_number == number: 17 | print("You guessed correctly!") 18 | elif number - user_number in (1, -1): 19 | print("You were off by 1.") 20 | else: 21 | print("Sorry, it's wrong!") 22 | 23 | # -- 24 | 25 | # We could also do a transformation instead of checking multiple options. 26 | 27 | number = 7 28 | user_input = input("Enter 'y' if you would like to play: ") 29 | 30 | if user_input.lower() == "y": 31 | user_number = int(input("Guess our number: ")) 32 | if user_number == number: 33 | print("You guessed correctly!") 34 | elif abs(number - user_number) == 1: 35 | print("You were off by 1.") 36 | else: 37 | print("Sorry, it's wrong!") 38 | -------------------------------------------------------------------------------- /19_lambda_functions/code.py: -------------------------------------------------------------------------------- 1 | def add(x, y): 2 | return x + y 3 | 4 | 5 | print(add(5, 7)) 6 | 7 | # -- Written as a lambda -- 8 | 9 | add = lambda x, y: x + y 10 | print(add(5, 7)) 11 | 12 | # Four parts 13 | # lambda 14 | # parameters 15 | # : 16 | # return value 17 | 18 | # Lambdas are meant to be short functions, often used without giving them a name. 19 | # For example, in conjunction with built-in function map 20 | # map applies the function to all values in the sequence 21 | 22 | 23 | def double(x): 24 | return x * 2 25 | 26 | 27 | sequence = [1, 3, 5, 9] 28 | 29 | doubled = [ 30 | double(x) for x in sequence 31 | ] # Put the result of double(x) in a new list, for each of the values in `sequence` 32 | doubled = map(double, sequence) 33 | print(list(doubled)) 34 | 35 | # -- Written as a lambda -- 36 | 37 | sequence = [1, 3, 5, 9] 38 | 39 | doubled = map(lambda x: x * 2, sequence) 40 | print(list(doubled)) 41 | 42 | # -- Important to remember -- 43 | # Lambdas are just functions without a name. 44 | # They are used to return a value calculated from its parameters. 45 | # Almost always single-line, so don't do anything complicated in them. 46 | # Very often better to just define a function and give it a proper name. 47 | -------------------------------------------------------------------------------- /21_unpacking_arguments/code.py: -------------------------------------------------------------------------------- 1 | def multiply(*args): 2 | print(args) 3 | total = 1 4 | for arg in args: 5 | total = total * arg 6 | 7 | return total 8 | 9 | 10 | print(multiply(3, 5)) 11 | print(multiply(-1)) 12 | 13 | # The asterisk takes all the arguments and packs them into a tuple. 14 | # The asterisk can be used to unpack sequences into arguments too! 15 | 16 | 17 | def add(x, y): 18 | return x + y 19 | 20 | 21 | nums = [3, 5] 22 | print(add(*nums)) # instead of add(nums[0], nums[1]) 23 | 24 | # -- Uses with keyword arguments -- 25 | # Double asterisk packs or unpacks keyword arguments 26 | 27 | 28 | def add(x, y): 29 | return x + y 30 | 31 | 32 | nums = {"x": 15, "y": 25} 33 | 34 | print(add(**nums)) 35 | 36 | # -- Forced named parameter -- 37 | 38 | 39 | def multiply(*args): 40 | total = 1 41 | for arg in args: 42 | total = total * arg 43 | 44 | return total 45 | 46 | 47 | def apply(*args, operator): 48 | if operator == "*": 49 | return multiply(args) 50 | elif operator == "+": 51 | return sum(args) 52 | else: 53 | return "No valid operator provided to apply()." 54 | 55 | 56 | print(apply(1, 3, 6, 7, operator="+")) 57 | print(apply(1, 3, 5, "+")) # Error 58 | -------------------------------------------------------------------------------- /25_str_and_repr/code.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, name, age): 3 | self.name = name 4 | self.age = age 5 | 6 | 7 | bob = Person("Bob", 35) 8 | print(bob) # Not the nicest thing to read! 9 | 10 | # -- __str__ -- 11 | # The goal of __str__ is to return a nice, easy to read string for end users. 12 | 13 | 14 | class Person: 15 | def __init__(self, name, age): 16 | self.name = name 17 | self.age = age 18 | 19 | def __str__(self): 20 | return f"Person {self.name}, {self.age} years old" 21 | 22 | 23 | bob = Person("Bob", 35) 24 | print(bob) # Much nicer 25 | 26 | # -- __repr__ -- 27 | # The goal of __repr__ is to be unambiguous, and if possible what it outputs should allow us to re-create an identical object. 28 | 29 | 30 | class Person: 31 | def __init__(self, name, age): 32 | self.name = name 33 | self.age = age 34 | 35 | def __repr__(self): 36 | # I'm adding the < > just so it's clear that this is an object we're printing out! 37 | return ( 38 | f"" 39 | ) # !r calls the __repr__ method of the thing. 40 | 41 | 42 | bob = Person("Bob", 35) 43 | print(bob) # Not as nice, but we could re-create "Bob" very easily. 44 | -------------------------------------------------------------------------------- /15_functions/code.py: -------------------------------------------------------------------------------- 1 | def hello(): 2 | print("Hello!") 3 | 4 | 5 | hello() 6 | 7 | # -- Defining vs. calling -- 8 | # It's still all sequential! 9 | 10 | 11 | def user_age_in_seconds(): 12 | user_age = int(input("Enter your age: ")) 13 | age_seconds = user_age * 365 * 24 * 60 * 60 14 | print(f"Your age in seconds is {age_seconds}.") 15 | 16 | 17 | print("Welcome to the age in seconds program!") 18 | user_age_in_seconds() 19 | 20 | print("Goodbye!") 21 | 22 | # -- Don't reuse names -- 23 | 24 | 25 | def print(): 26 | print("Hello, world!") # Error! 27 | 28 | 29 | # -- Don't reuse names, it's generally confusing! -- 30 | friends = ["Rolf", "Bob"] 31 | 32 | 33 | def add_friend(): 34 | friend_name = input("Enter your friend name: ") 35 | friends = friends + [friend_name] # Another way of adding to a list! 36 | 37 | 38 | add_friend() 39 | print(friends) # Always ['Rolf', 'Bob'] 40 | 41 | # -- Can't call a function before defining it -- 42 | 43 | say_hello() 44 | 45 | 46 | def say_hello(): 47 | print("Hello!") 48 | 49 | 50 | # -- Remember function body only runs when the function is called -- 51 | 52 | 53 | def add_friend(): 54 | friends.append("Rolf") 55 | 56 | 57 | friends = [] 58 | add_friend() 59 | 60 | print(friends) # [Rolf] 61 | -------------------------------------------------------------------------------- /16_function_arguments_parameters/code.py: -------------------------------------------------------------------------------- 1 | def add(x, y): 2 | result = x + y 3 | print(result) 4 | 5 | 6 | add(2, 3) # 5 7 | 8 | # -- If a function doesn't have parameter, you can't give it arguments -- 9 | 10 | 11 | def say_hello(): 12 | print("Hello!") 13 | 14 | 15 | say_hello("Bob") # Error 16 | 17 | # -- But if you add a parameter, then you must give it an argument -- 18 | 19 | 20 | def say_hello(name): 21 | print(f"Hello, {name}!") 22 | 23 | 24 | say_hello("Bob") 25 | say_hello() # Error, needs an argument 26 | 27 | # -- Keyword arguments -- 28 | # To make things clearer, in Python you can give keyword arguments. 29 | 30 | 31 | def say_hello(name): 32 | print(f"Hello, {name}!") 33 | 34 | 35 | say_hello(name="Bob") # Obvious that this is someone's name 36 | 37 | 38 | def divide(dividend, divisor): 39 | if divisor != 0: 40 | print(dividend / divisor) 41 | else: 42 | print("You fool!") 43 | 44 | 45 | divide(dividend=15, divisor=3) 46 | divide(15, 0) 47 | divide(15, divisor=0) # That's OK 48 | # divide(dividend=15, 0) # Not OK, named arguments must go after positional arguments 49 | 50 | # I recommend you use keyword arguments whenever possible, just because it makes things much more readable and maintainable long-term. 51 | -------------------------------------------------------------------------------- /14_destructuring_variables/code.py: -------------------------------------------------------------------------------- 1 | x, y = 5, 11 2 | 3 | # x, y = (5, 11) 4 | 5 | # -- Destructuring in for loops -- 6 | 7 | student_attendance = {"Rolf": 96, "Bob": 80, "Anne": 100} 8 | 9 | for student, attendance in student_attendance.items(): 10 | print(f"{student}: {attendance}") 11 | 12 | 13 | # -- Another example -- 14 | 15 | people = [("Bob", 42, "Mechanic"), ("James", 24, "Artist"), ("Harry", 32, "Lecturer")] 16 | 17 | for name, age, profession in people: 18 | print(f"Name: {name}, Age: {age}, Profession: {profession}") 19 | 20 | 21 | # -- Much better than this! -- 22 | 23 | for person in people: 24 | print(f"Name: {person[0]}, Age: {person[1]}, Profession: {person[2]}") 25 | 26 | 27 | # -- Ignoring values with underscore -- 28 | 29 | person = ("Bob", 42, "Mechanic") 30 | name, _, profession = person 31 | 32 | print(name, profession) # Bob Mechanic 33 | 34 | 35 | # -- Collecting values -- 36 | 37 | head, *tail = [1, 2, 3, 4, 5] 38 | 39 | print(head) # 1 40 | print(tail) # [2, 3, 4, 5] 41 | 42 | 43 | *head, tail = [1, 2, 3, 4, 5] 44 | 45 | print(head) # [1, 2, 3, 4] 46 | print(tail) # 5 47 | 48 | # -- Packing and unpacking -- 49 | 50 | # This isn't actually the end of the story, as there are also ways to pack and unpack collections using * and **, but that's a bit more advanced and you'll learn about it later on! 51 | -------------------------------------------------------------------------------- /23_object_oriented_programming/code.py: -------------------------------------------------------------------------------- 1 | student = {"name": "Rolf", "grades": (89, 90, 93, 78, 90)} 2 | 3 | 4 | def average(sequence): 5 | return sum(sequence) / len(sequence) 6 | 7 | 8 | print(average(student["grades"])) 9 | 10 | # But wouldn't it be nice if we could... 11 | # print(student.average()) ? 12 | 13 | 14 | class Student: 15 | def __init__(self): 16 | self.name = "Rolf" 17 | self.grades = (89, 90, 93, 78, 90) 18 | 19 | def average(self): 20 | return sum(self.grades) / len(self.grades) 21 | 22 | 23 | student = Student() 24 | print(student.average()) 25 | # Identical to Student.average(student) 26 | 27 | 28 | # -- Parameters in __init__ -- 29 | 30 | 31 | class Student: 32 | def __init__(self, name, grades): 33 | self.name = name 34 | self.grades = grades 35 | 36 | def average(self): 37 | return sum(self.grades) / len(self.grades) 38 | 39 | 40 | student = Student("Bob", (36, 67, 90, 100, 100)) 41 | print(student.average()) 42 | 43 | # -- Remember *args ? -- 44 | 45 | 46 | class Student: 47 | def __init__(self, name, *grades): 48 | self.name = name 49 | self.grades = grades 50 | 51 | def average(self): 52 | return sum(self.grades) / len(self.grades) 53 | 54 | 55 | student = Student("Bob", 36, 67, 90, 100, 100) 56 | print(student.average()) 57 | -------------------------------------------------------------------------------- /27_class_inheritance/code.py: -------------------------------------------------------------------------------- 1 | class Device: 2 | def __init__(self, name, connected_by): 3 | self.name = name 4 | self.connected_by = connected_by 5 | self.connected = True 6 | 7 | def __str__(self): 8 | return f"Device {self.name!r} ({self.connected_by})" 9 | 10 | def disconnect(self): 11 | self.connected = False 12 | 13 | 14 | # printer = Device("Printer", "USB") 15 | # print(printer) 16 | 17 | # We don't want to add printer-specific stuff to Device, so... 18 | 19 | 20 | class Printer(Device): 21 | def __init__(self, name, connected_by, capacity): 22 | # super(Printer, self).__init__(name, connected_by) - Python2.7 23 | super().__init__(name, connected_by) # Python3+ 24 | self.capacity = capacity 25 | self.remaining_pages = capacity 26 | 27 | def __str__(self): 28 | return f"{super().__str__()} ({self.remaining_pages} pages remaining)" 29 | 30 | def print(self, pages): 31 | if not self.connected: 32 | raise TypeError("Device is disconnected at this time, cannot print.") 33 | print(f"Printing {pages} pages.") 34 | self.remaining_pages -= pages 35 | 36 | 37 | printer = Printer("Printer", "USB", 500) 38 | printer.print(20) 39 | print(printer) 40 | printer.print(50) 41 | print(printer) 42 | printer.disconnect() 43 | printer.print(30) # Error 44 | -------------------------------------------------------------------------------- /41_mutable_default_parameters/code.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class Student: 5 | def __init__(self, name: str, grades: List[int] = []): # This is bad! 6 | self.name = name 7 | self.grades = grades 8 | 9 | def take_exam(self, result): 10 | self.grades.append(result) 11 | 12 | 13 | bob = Student("Bob") 14 | rolf = Student("Rolf") 15 | bob.take_exam(90) 16 | print(bob.grades) 17 | print(rolf.grades) # Whaaaaaat 18 | 19 | # The function parameters evaluate when the function is defined, not when it runs. 20 | # That means that self.grades is a name for the list that was evaluated when the function was defined. 21 | # We're then modifying it in take_exam 22 | # But all calls to the __init__ method have the same list (because parameters are only evaluated once!) 23 | # So all students have the same list 24 | 25 | # Avoid it by not having mutable parameters. Instead, do what we did in prior lectures: 26 | 27 | from typing import List 28 | 29 | 30 | class Student: 31 | def __init__(self, name: str, grades: List[int] = None): 32 | self.name = name 33 | self.grades = grades or [] # New list created if one isn't passed 34 | 35 | def take_exam(self, result): 36 | self.grades.append(result) 37 | 38 | 39 | bob = Student("Bob") 40 | rolf = Student("Rolf") 41 | bob.take_exam(90) 42 | print(bob.grades) 43 | print(rolf.grades) # Now it's empty. 44 | -------------------------------------------------------------------------------- /22_unpacking_keyword_arguments/code.py: -------------------------------------------------------------------------------- 1 | # -- Unpacking kwargs -- 2 | def named(**kwargs): 3 | print(kwargs) 4 | 5 | 6 | named(name="Bob", age=25) 7 | # named({"name": "Bob", "age": 25}) # Error, the dictionary is actually a positional argument. 8 | 9 | # Unpack dict into arguments. This is OK, but slightly more confusing. Good when working with variables though. 10 | named(**{"name": "Bob", "age": 25}) 11 | 12 | 13 | # -- Unpacking and repacking -- 14 | def named(**kwargs): 15 | print(kwargs) 16 | 17 | 18 | def print_nicely(**kwargs): 19 | named(**kwargs) # Unpack the dictionary into keyword arguments. 20 | for arg, value in kwargs.items(): 21 | print(f"{arg}: {value}") 22 | 23 | 24 | print_nicely(name="Bob", age=25) 25 | 26 | 27 | # -- Both args and kwargs -- 28 | 29 | 30 | def both(*args, **kwargs): 31 | print(args) 32 | print(kwargs) 33 | 34 | 35 | both(1, 3, 5, name="Bob", age=25) 36 | 37 | # This is normally used to accept an unlimited number of arguments and keyword arguments, such that some of them can be passed onto other functions. 38 | # You'll frequently see things like these in Python code: 39 | 40 | """ 41 | def post(url, data=None, json=None, **kwargs): 42 | return request('post', url, data=data, json=json, **kwargs) 43 | """ 44 | 45 | # While the implementation is irrelevant at this stage, what it allows is for the caller of `post()` to pass arguments to `request()`. 46 | 47 | # -- Error when unpacking -- 48 | 49 | 50 | def myfunction(**kwargs): 51 | print(kwargs) 52 | 53 | 54 | myfunction(**"Bob") # Error, must be mapping 55 | myfunction(**None) # Error 56 | -------------------------------------------------------------------------------- /26_classmethod_staticmethod/code.py: -------------------------------------------------------------------------------- 1 | class ClassTest: 2 | def instance_method(self): 3 | print(f"Called instance_method of {self}") 4 | 5 | @classmethod 6 | def class_method(cls): 7 | print(f"Called class_method of {cls}") 8 | 9 | @staticmethod 10 | def static_method(): 11 | print(f"Called static_method. We don't get any object or class info here.") 12 | 13 | 14 | instance = ClassTest() 15 | instance.instance_method() 16 | 17 | ClassTest.class_method() 18 | ClassTest.static_method() 19 | 20 | # -- What are they used for? -- 21 | 22 | # Instance methods are used for most things. When you want to produce an action that uses the data stored in an object. 23 | # Static methods are used to just place a method inside a class because you feel it belongs there (i.e. for code organisation, mostly!) 24 | # Class methods are often used as factories. 25 | 26 | 27 | class Book: 28 | TYPES = ("hardcover", "paperback") 29 | 30 | def __init__(self, name, book_type, weight): 31 | self.name = name 32 | self.book_type = book_type 33 | self.weight = weight 34 | 35 | def __repr__(self): 36 | return f"" 37 | 38 | @classmethod 39 | def hardcover(cls, name, page_weight): 40 | return cls(name, cls.TYPES[0], page_weight + 100) 41 | 42 | @classmethod 43 | def paperback(cls, name, page_weight): 44 | return cls(name, cls.TYPES[1], page_weight) 45 | 46 | 47 | heavy = Book.hardcover("Harry Potter", 1500) 48 | light = Book.paperback("Python 101", 600) 49 | 50 | print(heavy) 51 | print(light) 52 | -------------------------------------------------------------------------------- /11_loops/code.py: -------------------------------------------------------------------------------- 1 | # -- While loop -- 2 | 3 | number = 7 4 | play = input("Would you like to play? (Y/n) ") 5 | 6 | while play != "n": 7 | user_number = int(input("Guess our number: ")) 8 | if user_number == number: 9 | print("You guessed correctly!") 10 | elif abs(number - user_number) == 1: 11 | print("You were off by 1.") 12 | else: 13 | print("Sorry, it's wrong!") 14 | 15 | play = input("Would you like to play? (Y/n) ") 16 | 17 | 18 | # -- The break keyword -- 19 | 20 | while True: 21 | play = input("Would you like to play? (Y/n) ") 22 | 23 | if play == "n": 24 | break # Exit the loop 25 | 26 | user_number = int(input("Guess our number: ")) 27 | if user_number == number: 28 | print("You guessed correctly!") 29 | elif abs(number - user_number) == 1: 30 | print("You were off by 1.") 31 | else: 32 | print("Sorry, it's wrong!") 33 | 34 | 35 | # -- For loop -- 36 | 37 | friends = ["Rolf", "Jen", "Bob", "Anne"] 38 | for friend in friends: 39 | print(f"{friend} is my friend.") 40 | 41 | # -- For loop 2 -- Average 42 | 43 | grades = [35, 67, 98, 100, 100] 44 | total = 0 45 | amount = len(grades) 46 | 47 | for grade in grades: 48 | total += grade 49 | 50 | print(total / amount) 51 | 52 | # -- Rewritten using sum() -- 53 | 54 | grades = [35, 67, 98, 100, 100] 55 | total = sum(grades) 56 | amount = len(grades) 57 | 58 | print(total / amount) 59 | 60 | # You kinda just have to "know" that exists. It takes time and experience, but searching for almost _everything_ really helps. For example, you could've searched for "sum list of numbers python". 61 | -------------------------------------------------------------------------------- /37_decorating_functions_with_parameters/code.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | user = {"username": "jose", "access_level": "guest"} 5 | 6 | 7 | def make_secure(func): 8 | @functools.wraps(func) 9 | def secure_function(panel): 10 | if user["access_level"] == "admin": 11 | return func(panel) 12 | else: 13 | return f"No admin permissions for {user['username']}." 14 | 15 | return secure_function 16 | 17 | 18 | @make_secure 19 | def get_password(panel): 20 | if panel == "admin": 21 | return "1234" 22 | elif panel == "billing": 23 | return "super_secure_password" 24 | 25 | 26 | # print(get_password("admin")) # Error before adding parameters, works after 27 | # But now we've coupled our function to our decorator. We can't decorate a different function, which isn't great! 28 | # Instead we could take unlimited parameters and pass whatever we get down to the original function 29 | 30 | 31 | def make_secure(func): 32 | @functools.wraps(func) 33 | def secure_function(*args, **kwargs): 34 | if user["access_level"] == "admin": 35 | return func(*args, **kwargs) 36 | else: 37 | return f"No admin permissions for {user['username']}." 38 | 39 | return secure_function 40 | 41 | 42 | @make_secure 43 | def get_password(panel): 44 | if panel == "admin": 45 | return "1234" 46 | elif panel == "billing": 47 | return "super_secure_password" 48 | 49 | 50 | print(get_password("admin")) 51 | print(get_password("billing")) 52 | 53 | user = {"username": "bob", "access_level": "admin"} 54 | 55 | print(get_password("admin")) 56 | print(get_password("billing")) 57 | -------------------------------------------------------------------------------- /33_custom_error_classes/code.py: -------------------------------------------------------------------------------- 1 | class Book: 2 | def __init__(self, name: str, page_count: int): 3 | self.name = name 4 | self.page_count = page_count 5 | self.pages_read = 0 6 | 7 | def __repr__(self): 8 | return ( 9 | f"" 10 | ) 11 | 12 | def read(self, pages: int): 13 | self.pages_read += pages 14 | print(f"You have now read {self.pages_read} pages out of {self.page_count}") 15 | 16 | 17 | python101 = Book("Python 101", 50) 18 | python101.read(35) 19 | python101.read(50) # Whaaaat 20 | 21 | # -- Errors used to prevent things from happening -- 22 | 23 | 24 | class TooManyPagesReadError(ValueError): 25 | pass 26 | 27 | 28 | class Book: 29 | def __init__(self, name: str, page_count: int): 30 | self.name = name 31 | self.page_count = page_count 32 | self.pages_read = 0 33 | 34 | def __repr__(self): 35 | return ( 36 | f"" 37 | ) 38 | 39 | def read(self, pages: int): 40 | if self.pages_read + pages > self.page_count: 41 | raise TooManyPagesReadError( 42 | f"You tried to read {self.pages_read + pages} pages, but this book only has {self.page_count} pages." 43 | ) 44 | self.pages_read += pages 45 | print(f"You have now read {self.pages_read} pages out of {self.page_count}") 46 | 47 | 48 | python101 = Book("Python 101", 50) 49 | python101.read(35) 50 | python101.read( 51 | 50 52 | ) # This now raises an error, which has a helpful name and a helpful error message. 53 | -------------------------------------------------------------------------------- /29_type_hinting/code.py: -------------------------------------------------------------------------------- 1 | def list_avg(sequence: list) -> float: 2 | return sum(sequence) / len(sequence) 3 | 4 | 5 | # -- Type hinting classes -- 6 | 7 | 8 | class Book: 9 | def __init__(self, name: str, page_count: int): 10 | self.name = name 11 | self.page_count = page_count 12 | 13 | 14 | # -- Lists and collections -- 15 | 16 | from typing import List # , Tuple, Set, etc... 17 | 18 | 19 | class BookShelf: 20 | def __init__(self, books: List[Book]): 21 | self.books = books 22 | 23 | def __str__(self) -> str: 24 | return f"BookShelf with {len(self.books)} books." 25 | 26 | 27 | # Key benefit is now you'll get told if you pass in the wrong thing... 28 | 29 | book = Book( 30 | "Harry Potter", "352" 31 | ) # Suggests this is incorrect if you have a tool that will analyse your code (e.g. PyCharm or Pylint) 32 | shelf = BookShelf(book) # Suggests this is incorrect too 33 | # Type hinting is that: hints. It doesn't stop your code from working... although it can save you at times! 34 | 35 | # -- Hinting the current object -- 36 | 37 | 38 | class Book: 39 | TYPES = ("hardcover", "paperback") 40 | 41 | def __init__(self, name: str, book_type: str, weight: int): 42 | self.name = name 43 | self.book_type = book_type 44 | self.weight = weight 45 | 46 | def __repr__(self) -> str: 47 | return f"" 48 | 49 | @classmethod 50 | def hardcover(cls, name: str, page_weight: int) -> "Book": 51 | return cls(name, cls.TYPES[0], page_weight + 100) 52 | 53 | @classmethod 54 | def paperback(cls, name: str, page_weight: int) -> "Book": 55 | return cls(name, cls.TYPES[1], page_weight) 56 | -------------------------------------------------------------------------------- /38_decorators_with_parameters/code.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | user = {"username": "anna", "access_level": "user"} 4 | 5 | 6 | def make_secure(func): 7 | @functools.wraps(func) 8 | def secure_function(*args, **kwargs): 9 | if user["access_level"] == "admin": 10 | return func(*args, **kwargs) 11 | else: 12 | return f"No admin permissions for {user['username']}." 13 | 14 | return secure_function 15 | 16 | 17 | @make_secure 18 | def get_admin_password(): 19 | return "admin: 1234" 20 | 21 | 22 | @make_secure 23 | def get_dashboard_password(): 24 | return "user: user_password" 25 | 26 | 27 | # What if we wanted some passwords to be available to "user" and others to "admin" ? 28 | 29 | user = {"username": "anna", "access_level": "user"} 30 | 31 | 32 | def make_secure(access_level): 33 | def decorator(func): 34 | @functools.wraps(func) 35 | def secure_function(*args, **kwargs): 36 | if user["access_level"] == access_level: 37 | return func(*args, **kwargs) 38 | else: 39 | return f"No {access_level} permissions for {user['username']}." 40 | 41 | return secure_function 42 | 43 | return decorator 44 | 45 | 46 | @make_secure( 47 | "admin" 48 | ) # This runs the make_secure function, which returns decorator. Essentially the same to doing `@decorator`, which is what we had before. 49 | def get_admin_password(): 50 | return "admin: 1234" 51 | 52 | 53 | @make_secure("user") 54 | def get_dashboard_password(): 55 | return "user: user_password" 56 | 57 | 58 | print(get_admin_password()) 59 | print(get_dashboard_password()) 60 | 61 | user = {"username": "anna", "access_level": "admin"} 62 | 63 | print(get_admin_password()) 64 | print(get_dashboard_password()) 65 | -------------------------------------------------------------------------------- /40_mutability/code.py: -------------------------------------------------------------------------------- 1 | a = [] 2 | b = a 3 | 4 | # Remember a and b are _names_ for the list. They both have the _same_ value. 5 | 6 | a.append(35) # Modify the value. 7 | 8 | print(a) 9 | print(b) 10 | 11 | # We mutated (changed) the value, its names still point to the _same thing_, so it doesn't matter which name you use. 12 | 13 | a = [] 14 | b = [] 15 | 16 | a.append(35) 17 | 18 | print(a) 19 | print(b) 20 | 21 | # Here they are different lists, because [] creates a new list every time. You can check whether two things are the _same_ one by usingt the `id()` function: 22 | 23 | print(id(a)) 24 | print(id(b)) # Different from id(a) 25 | 26 | # -- immutable -- 27 | 28 | # Some values can't be changed because they don't have methods that modify the value itself. 29 | # In case of the list, `.append()` mutates the list. 30 | # For example integers don't have any such methods, so they are called _immutable_. 31 | 32 | a = 8597 33 | b = 8597 34 | 35 | print(id(a)) 36 | print(id(b)) # Same one 37 | 38 | a = 8598 39 | 40 | print(id(a)) 41 | print( 42 | id(b) 43 | ) # Different, because we didn't change 8597. We just used the name 'a' for a different value. 'b' still is a name for 8597. 44 | 45 | # Most things are mutable in Python. If you want to keep one of your classes immutable, don't add any methods that change the objects' properties. 46 | 47 | # Tuples and strings are the only fundamental collection in Python which is immutable. 48 | # Lists, sets, dictionaries are all mutable. 49 | # Integers, floats, and booleans are all immutable. 50 | 51 | # -- += and similar -- 52 | 53 | # A lot of beginners think this: 54 | 55 | a = "hello" 56 | b = a 57 | 58 | print(id(a)) 59 | print(id(b)) 60 | 61 | a += "world" 62 | 63 | # Would cause 'b' to change 64 | # But it doesn't, because strings are immutable. When you do str + str, a _new_ string is created. 65 | # This means that a becomes a new string containing "helloworld", but b still is a name for "hello". 66 | 67 | print(id(a)) 68 | print(id(b)) 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Course template 2 | 3 | ## Set up project 4 | 5 | Make sure you have Python3.7 installed. 6 | 7 | Then, in a terminal, run: 8 | 9 | ``` 10 | pipenv install 11 | ``` 12 | 13 | ### Set up Git Hooks 14 | 15 | We have a Git hook config that will run Black formatter on all modified files **before you commit**—that's in case you forget to do so yourself or don't have your IDE set up to format on save (which you should, Black is unintrusive!). 16 | 17 | To set up the Git hook: 18 | 19 | ``` 20 | pipenv shell 21 | pre-commit install 22 | ``` 23 | 24 | The first time you commit it may take a little longer than you'd hope, but subsequent commits will be quicker. 25 | 26 | > Commits will **fail** if any files are formatted, but the files will still be formatted. This is so you are aware of the reformat and also so we don't accidentally reformat something we don't want to reformat. 27 | 28 | If you want to run the commit without running the hook, do `git commit --no-verify`. This is highly discouraged, we should be aiming for consistent code, and using a formatter really helps us achieve that! 29 | 30 | ## allow_prereleases 31 | 32 | The Pipfile has `allow_prereleases = "true"` because for Black this is required. Make sure when installing libraries that the latest stable version is used, but try to avoid using alpha and beta versions where possible. 33 | 34 | There are times where it will be unavoidable to use prerelease versions of libraries used in a course to teach actual content. We should discuss them on an individual basis. The reason for this is that if we cover an alpha version of a library, it's likely it will change by the time the course is released. 35 | 36 | ## Creating course documentation 37 | 38 | Internal course documentation (for recording purposes) should go in Notion ((https://www.notion.so/teclado/Course-Wikis-fdd4b72cb5f74e1a9d7172f03e5c24cd)[https://www.notion.so/teclado/Course-Wikis-fdd4b72cb5f74e1a9d7172f03e5c24cd]). 39 | 40 | If there is documentation or files that students would benefit from having, they should be included in this repository alongside the code. 41 | -------------------------------------------------------------------------------- /34_first_class_functions/code.py: -------------------------------------------------------------------------------- 1 | # A first class function just means that functions can be passed as arguments to functions. 2 | 3 | 4 | def calculate(*values, operator): 5 | return operator(*values) 6 | 7 | 8 | def divide(dividend, divisor): 9 | if divisor != 0: 10 | return dividend / divisor 11 | else: 12 | return "You fool!" 13 | 14 | 15 | # We pass the `divide` function as an argument 16 | result = calculate(20, 4, operator=divide) 17 | print(result) 18 | 19 | 20 | def average(*values): 21 | return sum(values) / len(values) 22 | 23 | 24 | result = calculate(10, 20, 30, 40, operator=average) 25 | print(result) 26 | 27 | # -- searching with first-class functions -- 28 | 29 | 30 | def search(sequence, expected, finder): 31 | for elem in sequence: 32 | if finder(elem) == expected: 33 | return elem 34 | raise RuntimeError(f"Could not find an element with {expected}") 35 | 36 | 37 | friends = [ 38 | {"name": "Rolf Smith", "age": 24}, 39 | {"name": "Adam Wool", "age": 30}, 40 | {"name": "Anne Pun", "age": 27}, 41 | ] 42 | 43 | 44 | def get_friend_name(friend): 45 | return friend["name"] 46 | 47 | 48 | print(search(friends, "Bob Smith", get_friend_name)) 49 | 50 | # -- using lambdas since this can be simple enough -- 51 | 52 | 53 | def search(sequence, expected, finder): 54 | for elem in sequence: 55 | if finder(elem) == expected: 56 | return elem 57 | raise RuntimeError(f"Could not find an element with {expected}") 58 | 59 | 60 | friends = [ 61 | {"name": "Rolf Smith", "age": 24}, 62 | {"name": "Adam Wool", "age": 30}, 63 | {"name": "Anne Pun", "age": 27}, 64 | ] 65 | 66 | print(search(friends, "Bob Smith", lambda friend: friend["name"])) 67 | 68 | 69 | # -- or as an extra, using built-in functions -- 70 | 71 | from operator import itemgetter 72 | 73 | 74 | def search(sequence, expected, finder): 75 | for elem in sequence: 76 | if finder(elem) == expected: 77 | return elem 78 | raise RuntimeError(f"Could not find an element with {expected}") 79 | 80 | 81 | friends = [ 82 | {"name": "Rolf Smith", "age": 24}, 83 | {"name": "Adam Wool", "age": 30}, 84 | {"name": "Anne Pun", "age": 27}, 85 | ] 86 | 87 | print(search(friends, "Rolf Smith", itemgetter("name"))) 88 | -------------------------------------------------------------------------------- /32_errors_in_python/code.py: -------------------------------------------------------------------------------- 1 | def divide(dividend, divisor): 2 | if divisor == 0: 3 | raise ZeroDivisionError("Divisor cannot be 0.") 4 | 5 | return dividend / divisor 6 | 7 | 8 | grades = [] # Imagine we have no grades yet 9 | # average = divide(sum(grades) / len(grades)) # Error! 10 | 11 | try: 12 | average = divide(sum(grades), len(grades)) 13 | print(average) 14 | except ZeroDivisionError as e: 15 | print(e) 16 | # Much friendlier error message because now we're dealing with it 17 | # In a "students and grades" context, not purely in a mathematical context 18 | # I.e. it doesn't make sense to put "There are no grades yet in your list" 19 | # inside the `divide` function, because you could be dividing something 20 | # that isn't grades, in another program. 21 | print("There are no grades yet in your list.") 22 | 23 | # -- Built-in errors -- 24 | 25 | # TypeError: something was the wrong type 26 | # ValueError: something had the wrong value 27 | # RuntimeError: most other things 28 | 29 | # Full list of built-in errors: https://docs.python.org/3/library/exceptions.html 30 | 31 | 32 | # -- Doing something if no error is raised -- 33 | 34 | grades = [90, 100, 85] 35 | 36 | try: 37 | average = divide(sum(grades), len(grades)) 38 | except ZeroDivisionError: 39 | print("There are no grades yet in your list.") 40 | else: 41 | print(f"The average was {average}") 42 | 43 | 44 | # -- Doing something no matter what -- 45 | # This is particularly useful when dealing with resources that you open and then must close 46 | # The `finally` part always runs, so you could use it to close things down 47 | # You can also use it to print something at the end of your try-block if you like. 48 | 49 | students = [ 50 | {"name": "Bob", "grades": [75, 90]}, 51 | {"name": "Rolf", "grades": []}, 52 | {"name": "Jen", "grades": [100, 90]}, 53 | ] 54 | 55 | try: 56 | for student in students: 57 | name = student["name"] 58 | grades = student["grades"] 59 | average = divide(sum(grades), len(grades)) 60 | print(f"{name} averaged {average}.") 61 | except ZeroDivisionError: 62 | print(f"ERROR: {name} has no grades!") 63 | else: 64 | print("-- All student averages calculated --") 65 | finally: 66 | print("-- End of student average calculation --") 67 | -------------------------------------------------------------------------------- /35_simple_decorators_python/code.py: -------------------------------------------------------------------------------- 1 | user = {"username": "jose", "access_level": "guest"} 2 | 3 | 4 | def get_admin_password(): 5 | return "1234" 6 | 7 | 8 | print(get_admin_password()) # Can do this even though I'm a "guest" 9 | 10 | # Now this only runs if I'm an admin... but 11 | if user["access_level"] == "admin": 12 | print(get_admin_password()) 13 | 14 | print(get_admin_password()) # The function itself is still unsecured 15 | 16 | # -- "secure" function -- 17 | 18 | 19 | def secure_get_admin(): 20 | if user["access_level"] == "admin": 21 | print(get_admin_password()) 22 | 23 | 24 | # Now secure_get_admin() is secure. 25 | # But get_admin_password() is still around, and I could call it: 26 | 27 | secure_get_admin() 28 | print(get_admin_password()) 29 | 30 | # We want to get rid of get_admin_password so that only the secure function remains! 31 | # Maybe something like this? 32 | 33 | 34 | def secure_function(func): 35 | if user["access_level"] == "admin": 36 | return func 37 | 38 | 39 | user = {"username": "bob", "access_level": "admin"} 40 | 41 | get_admin_password = secure_function(get_admin_password) 42 | print(get_admin_password()) # Error! 43 | 44 | # When we ran `secure_function`, we checked the user's access level. Because at that point the user was not an admin, the function did not `return func`. Therefore `get_admin_password` is set to `None`. 45 | 46 | # We want to delay overwriting until we run the function 47 | 48 | 49 | def get_admin_password(): 50 | return "1234" 51 | 52 | 53 | def make_secure(func): 54 | def secure_function(): 55 | if user["access_level"] == "admin": 56 | return func() 57 | 58 | return secure_function 59 | 60 | 61 | get_admin_password = make_secure( 62 | get_admin_password 63 | ) # `get_admin_password` is now `secure_func` from above 64 | 65 | user = {"username": "jose", "access_level": "guest"} 66 | print(get_admin_password()) # Now we check access level 67 | 68 | user = {"username": "bob", "access_level": "admin"} 69 | print(get_admin_password()) # Now we check access level 70 | 71 | # -- More information or error handling -- 72 | 73 | 74 | def get_admin_password(): 75 | return "1234" 76 | 77 | 78 | def make_secure(func): 79 | def secure_function(): 80 | if user["access_level"] == "admin": 81 | return func() 82 | else: 83 | return f"No admin permissions for {user['username']}." 84 | 85 | return secure_function 86 | 87 | 88 | get_admin_password = make_secure( 89 | get_admin_password 90 | ) # `get_admin_password` is now `secure_func` from above 91 | 92 | user = {"username": "jose", "access_level": "guest"} 93 | print(get_admin_password()) # Now we check access level 94 | 95 | user = {"username": "bob", "access_level": "admin"} 96 | print(get_admin_password()) # Now we check access level 97 | -------------------------------------------------------------------------------- /03_getting_user_input/slide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Code 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |

User input

38 |

39 |               size_input = input("How big is your house (in square feet): ")
40 |               square_feet = int(size)
41 |               square_metres = square_feet * 15  # Make sure this is correct
42 |               print(f"{square_feet} square feet is {square_metres} square metres.")
43 | 					
44 |
45 | 46 | 47 |
48 |

User input

49 |

50 |               size_input = input("How big is your house (in square feet): ")
51 |               square_feet = int(size)
52 |               square_metres = square_feet * 15  # Make sure this is correct
53 |               print(f"{square_feet} square feet is {square_metres} square metres.")
54 |           
55 |
56 | 57 |
58 |

User input to numbers

59 |

60 |               size_input = input("How big is your house (in square feet): ")
61 |               square_feet = int(size)
62 |               square_metres = square_feet * 15  # Make sure this is correct
63 |               print(f"{square_feet} square feet is {square_metres} square metres.")
64 |           
65 |
66 |
67 |
68 | 69 | 70 | 71 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /02_string_formatting/slide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Code 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |

String formatting

38 |

 39 |             name = "Rolf"
 40 |             greeting = f"Hello, {name}"
 41 | 
 42 |             print(greeting)  # "Hello, Rolf"
 43 |             print(f"Hello, {name}")  # "Hello, Rolf"
 44 | 					
45 |
46 | 47 | 48 |
49 |

String formatting

50 |

 51 |             name = "Rolf"
 52 |             greeting = f"Hello, {name}"
 53 | 
 54 |             print(greeting)  # "Hello, Rolf"
 55 |             print(f"Hello, {name}")  # "Hello, Rolf"
 56 |           
57 |
58 | 59 |
60 |

String formatting

61 |

 62 |               name = "Rolf"
 63 |               greeting = f"Hello, {name}"
 64 |   
 65 |               print(greeting)  # "Hello, Rolf"
 66 |               print(f"Hello, {name}")  # "Hello, Rolf"
 67 |             
68 |
69 | 70 |
71 |

Reusable formatting

72 |

 73 |             longer_phrase = "Hello, {}. Today is {}."
 74 |             formatted = longer_phrase.format("Rolf", "Monday")
 75 |             print(formatted)  # Hello, Rolf. Today is Monday.
 76 |             
77 |
78 |
79 |

Reusable formatting

80 |

 81 |             longer_phrase = "Hello, {}. Today is {}."
 82 |             formatted = longer_phrase.format("Rolf", "Monday")
 83 |             print(formatted)  # Hello, Rolf. Today is Monday.
 84 |             
85 |
86 |
87 |
88 | 89 | 90 | 91 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d8eea0ca3b4a555de0fb7bcba57e13411f8f0ce34ec3246550162f69f2f79a2d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": { 20 | "appdirs": { 21 | "hashes": [ 22 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 23 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 24 | ], 25 | "version": "==1.4.3" 26 | }, 27 | "aspy.yaml": { 28 | "hashes": [ 29 | "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3", 30 | "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482" 31 | ], 32 | "version": "==1.2.0" 33 | }, 34 | "attrs": { 35 | "hashes": [ 36 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 37 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 38 | ], 39 | "version": "==19.1.0" 40 | }, 41 | "black": { 42 | "hashes": [ 43 | "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", 44 | "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" 45 | ], 46 | "index": "pypi", 47 | "version": "==19.3b0" 48 | }, 49 | "cfgv": { 50 | "hashes": [ 51 | "sha256:6e9f2feea5e84bc71e56abd703140d7a2c250fc5ba38b8702fd6a68ed4e3b2ef", 52 | "sha256:e7f186d4a36c099a9e20b04ac3108bd8bb9b9257e692ce18c8c3764d5cb12172" 53 | ], 54 | "version": "==1.6.0" 55 | }, 56 | "click": { 57 | "hashes": [ 58 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 59 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 60 | ], 61 | "version": "==7.0" 62 | }, 63 | "identify": { 64 | "hashes": [ 65 | "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd", 66 | "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171" 67 | ], 68 | "version": "==1.4.1" 69 | }, 70 | "importlib-metadata": { 71 | "hashes": [ 72 | "sha256:46fc60c34b6ed7547e2a723fc8de6dc2e3a1173f8423246b3ce497f064e9c3de", 73 | "sha256:bc136180e961875af88b1ab85b4009f4f1278f8396a60526c0009f503a1a96ca" 74 | ], 75 | "version": "==0.9" 76 | }, 77 | "nodeenv": { 78 | "hashes": [ 79 | "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" 80 | ], 81 | "version": "==1.3.3" 82 | }, 83 | "pre-commit": { 84 | "hashes": [ 85 | "sha256:75a9110eae00d009c913616c0fc8a6a02e7716c4a29a14cac9b313d2c7338ab0", 86 | "sha256:f882c65316eb5b705fe4613e92a7c91055c1800102e4d291cfd18912ec9cf90e" 87 | ], 88 | "index": "pypi", 89 | "version": "==1.15.1" 90 | }, 91 | "pyyaml": { 92 | "hashes": [ 93 | "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", 94 | "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", 95 | "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", 96 | "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", 97 | "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", 98 | "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", 99 | "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", 100 | "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", 101 | "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", 102 | "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", 103 | "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" 104 | ], 105 | "version": "==5.1" 106 | }, 107 | "six": { 108 | "hashes": [ 109 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 110 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 111 | ], 112 | "version": "==1.12.0" 113 | }, 114 | "toml": { 115 | "hashes": [ 116 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 117 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 118 | ], 119 | "version": "==0.10.0" 120 | }, 121 | "virtualenv": { 122 | "hashes": [ 123 | "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", 124 | "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" 125 | ], 126 | "version": "==16.4.3" 127 | }, 128 | "zipp": { 129 | "hashes": [ 130 | "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478", 131 | "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4" 132 | ], 133 | "version": "==0.3.3" 134 | } 135 | } 136 | } 137 | --------------------------------------------------------------------------------