├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── pages.yaml │ └── tests.yaml ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── build.sh ├── content ├── 1-python-fundamentals │ ├── 0-unit-overview.html │ ├── 1-values-&-expressions.html │ ├── 10-exercise-conditionals.html │ ├── 11-user-input.html │ ├── 12-project-1-algorithms-as-decision-makers.html │ ├── 2-nested-call-expressions.html │ ├── 3-names-&-variables.html │ ├── 4-functions.html │ ├── 5-exercise-functions.html │ ├── 6-more-on-functions.html │ ├── 7-logical-expressions.html │ ├── 8-exercise-logical-expressions.html │ └── 9-conditionals.html ├── 2-loops-&-lists │ ├── 0-unit-overview.html │ ├── 1-while-loops.html │ ├── 10-exercise-nested-lists.html │ ├── 11-project-2-photo-filters.html │ ├── 2-exercise-while-loops.html │ ├── 3-for-loops-&-ranges.html │ ├── 4-lists.html │ ├── 5-looping-through-sequences.html │ ├── 6-exercise-loops-and-lists.html │ ├── 7-list-mutation.html │ ├── 7b-exercise-list-mutation.html │ ├── 8-more-on-lists.html │ └── 9-nested-lists.html ├── 3-strings-&-dictionaries │ ├── 0-unit-overview.html │ ├── 1-string-formatting.html │ ├── 2-exercise-string-formatting.html │ ├── 3-string-operations.html │ ├── 4-exercise-string-operations.html │ ├── 5-dictionaries.html │ ├── 6-exercise-dictionaries.html │ ├── 7-randomness.html │ ├── 8-files.html │ └── 9-project-3-text-generator.html ├── 4-object-oriented-programming │ ├── 0-unit-overview.html │ ├── 1-object-oriented-programming.html │ ├── 10-composition.html │ ├── 11-polymorphism.html │ ├── 12-project-4-oop-quiz.html │ ├── 2-classes.html │ ├── 3-exercise-classes.html │ ├── 4-more-on-classes.html │ ├── 5-exercise-more-on-classes.html │ ├── 6-inheritance.html │ ├── 7-exercise-inheritance.html │ ├── 8-more-on-inheritance.html │ └── 9-exercise-more-on-inheritance.html ├── _base.html.jinja2 ├── _code-exercise-script.html ├── _quiz-js-include.html ├── favicon.ico ├── images │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── callexpression.png │ ├── callexpression_nested.png │ ├── callexpression_tree.png │ ├── classes-chocolate-shop.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── inheritance-animal-diagram.png │ ├── inheritance-animal-pairs.png │ ├── more-on-functions-call-error.png │ ├── more-on-functions-call-repl.png │ ├── more-on-inheritance-layers.png │ ├── more-on-inheritance-object-base-class.png │ ├── object-oriented-programming-account-transfer.png │ ├── object-oriented-programming-account-types.png │ ├── pamelafox.jpg │ ├── randomness-pixels.png │ └── user-input.gif ├── index.html ├── lectures │ ├── debugging.html │ ├── examples │ │ ├── __init__.py │ │ ├── fox.py │ │ ├── input_number.py │ │ ├── input_number_test.py │ │ ├── quiz.py │ │ ├── scratch.py │ │ ├── sum_pos_scores.py │ │ ├── sum_pos_scores_test.py │ │ ├── sum_scores.py │ │ ├── sum_scores_test.py │ │ ├── test_sum_scores.py │ │ ├── texter.py │ │ └── weather.py │ ├── index.html │ ├── media │ │ ├── input_commandline.gif │ │ ├── name_value.png │ │ ├── pixel_grid.psd │ │ ├── screenshot_colab.png │ │ ├── screenshot_debugger.png │ │ ├── screenshot_exercise.png │ │ ├── screenshot_githubactions.jpg │ │ ├── screenshot_pythontutor.png │ │ └── software_testing_pyramid.png │ ├── testing.html │ ├── unit1.html │ ├── unit2.html │ ├── unit3.html │ ├── unit4.html │ └── workflow.html └── resources │ └── glossary.html ├── exercises ├── week1.ipynb ├── week2.ipynb ├── week3.ipynb └── week4.ipynb ├── projects ├── project1.ipynb ├── project2.ipynb ├── project2_vscode.ipynb ├── project3.ipynb └── project4.ipynb ├── pyproject.toml ├── requirements.txt ├── start.sh ├── tests ├── README.md ├── conftest.py └── test_e2e.py └── tutor.prompt.yaml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/python:3.11", 3 | "customizations": { 4 | "vscode": { 5 | "settings": { 6 | "terminal.integrated.fontSize": 18 7 | }, 8 | "extensions": [ 9 | "ms-python.python", 10 | "ms-python.vscode-pylance", 11 | "noahsyn10.pydoctestbtn" 12 | ] 13 | } 14 | }, 15 | "forwardPorts": [8000], 16 | "portsAttributes": { 17 | "8000": { 18 | "label": "Slides", 19 | "onAutoForward": "openPreview" 20 | } 21 | }, 22 | "postAttachCommand": "" 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/pages.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | # Allow one concurrent deployment 9 | concurrency: 10 | group: "pages" 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | deploy: 15 | environment: 16 | name: "github-pages" 17 | url: ${{ steps.deployment.outputs.page_url }} 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | pages: write 22 | id-token: write 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.11' 30 | cache: 'pip' 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements.txt 35 | - name: Build published content 36 | run: ./build.sh 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v5 39 | - name: Upload Artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: build 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: E2E tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: '3.13' 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install -r requirements.txt 19 | - name: Build published content 20 | run: ./build.sh 21 | - name: Ensure browsers are installed 22 | run: python -m playwright install chromium --with-deps 23 | - name: Run tests 24 | id: test 25 | run: python3 -m pytest 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__/ 3 | *.pyc 4 | .coverage 5 | .DS_Store 6 | build -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | python3 -m jinja2ssg --src articles --dest publish build 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proficient Python 2 | 3 | A comprehensive, interactive Python programming course designed to take you from beginner to proficient Python developer through hands-on learning. 4 | 5 | Visit [Proficient Python Course](https://proficientpython.com) to start learning. 6 | 7 | ## Key Features 8 | 9 | - **Progressive Learning Path**: Structured from basic to advanced concepts 10 | - **Interactive Exercises**: Practice coding with built-in exercises 11 | - **Hands-on Projects**: Four major projects to apply your skills 12 | - **Accessibility**: Designed with accessibility in mind 13 | 14 | ## Local Development 15 | 16 | To build and run the course locally: 17 | 18 | ```bash 19 | # Install dependencies 20 | pip install -r requirements.txt 21 | 22 | # Build the site 23 | ./build.sh 24 | 25 | # Run the local server 26 | ./start.sh 27 | ``` 28 | 29 | The course will be available at http://localhost:8000. 30 | 31 | ## Testing 32 | 33 | Run the accessibility and functionality tests: 34 | 35 | ```bash 36 | pytest 37 | ``` 38 | 39 | ## License 40 | 41 | This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/). 42 | 43 | ![CC BY-NC-SA 4.0](https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png) 44 | 45 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | python3 -m jinja2ssg --src content --dest build build 2 | cp -r content/images build/ 3 | cp -r content/favicon.ico build/ 4 | cp -r content/lectures/media build/lectures/ -------------------------------------------------------------------------------- /content/1-python-fundamentals/0-unit-overview.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Unit 1: Python Fundamentals{% endblock %} 3 | {% block content %} 4 |

Unit 1: Python Fundamentals

5 |

Welcome to your first unit of learning Python!

6 |

Our learning goals for this unit are:

7 | 12 |

At the end, you'll combine those skills to make a decision making algorithm.

13 | 14 |
15 | ➡️ Next up: Values & Expressions 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /content/1-python-fundamentals/1-values-&-expressions.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Values & expressions{% endblock %} 3 | {% block content %} 4 |

Values & expressions

5 |

Values

6 |

A computer program manipulates values. The earliest programs were number munchers, calculating mathematical formulas more efficiently than a calculator. These days, programs deal with user data, natural language text, BIG data sets, and all sorts of interesting values.

7 |

In Python and most languages, each value has a certain data type. Some basic types:

8 | 12 |

There’s a lot we can do with those four types, so we’ll start with those and introduce fancier types like lists and dictionaries in later units.

13 |

Expressions

14 |

Computer programs use expressions to manipulate values and come up with new values. Here are some Python expressions that use arithmetic operators to do calculations:

15 |
18 + 69     # Results in 87
16 | 
17 | 2021 - 37   # Results in 1984
18 | 
19 | 2 * 100     # Results in 200
20 | 
21 | 100/2       # Results in 50.0
22 | 
23 | 100//2      # Results in 50
24 | 
25 | 2 ** 100    # Results in 1267650600228229401496703205376
26 |

Most of those arithmetic operators likely look familiar to you, so let’s just break down the not so familiar ones.

27 |

The / operator is the “true division” operator; it treats both numbers as floating point numbers and always results in a floating point number (even if the first number evenly divided the second). Contrasted to that, // is the “floor division” operator; it calculates the result and then takes the “floor” of it, rounding down so that there is never anything after the decimal. It always results in an integer.

28 |

The ** operator is the exponentiation operator; it raises the first number to the power of the second number and returns the result. In math class, this is often written like 2^100, but Python decided to use ** instead. The ^ symbol is actually an operator in Python, but it's used for a very different operation (“bitwise exclusive or”) that we won’t dive into here.

29 |

You can put together multiple operators to make longer expressions, and use parentheses to group parts of the expression you want evaluated first. Parentheses work pretty much the same in programming as they do in math.

30 |
24 + ((60 * 6) / 12)
31 |

Try it: Magic birthday math

32 |

There's a neat math trick that we used to do back in the days of calculators. After a long sequence of calculations, your birthday will come out at the end as a floating point number! Try to use Python to pull off the math trick instead, using the editor below.

33 |

The instructions:

34 |
  1. Start with the number 7

  2. 35 |
  3. Multiply by the month of your birth

  4. 36 |
  5. Subtract 1

  6. 37 |
  7. Multiply by 13

  8. 38 |
  9. Add the day of your birth

  10. 39 |
  11. Add 3

  12. 40 |
  13. Multiply by 11

  14. 41 |
  15. Subtract the month of your birth

  16. 42 |
  17. Subtract the day of your birth

  18. 43 |
  19. Divide by 10

  20. 44 |
  21. Add 11

  22. 45 |
  23. Divide by 100

46 | 47 | # Type the expression below, and click "Run code" to see the result 48 | 7 49 |

String expressions

50 |

The most basic way to manipulate string values is concatenation: combining two strings together. If you've never heard that term, it's common in programming languages and comes from Latin "con" (with) + "catena" (chain). I always just think of "cadena", the Spanish word for necklace.

51 |
"red" + "blue"         # Results in "redblue"
52 |

If you want a whitespace between the strings you're concatenating, you need to explicitly put a space inside one of the strings:

53 |
"hola " + "mundo"      # Results in "hola mundo"
54 |

Or concatenate a string that is simply whitespace:

55 |
"oi" + " " + "galera"  # Results in "oi galera!"
56 |

String concatenation will soon become very useful, once we learn how to use variables and functions.

57 |

Try it: Say my name

58 |

Use Python to write an expression that results in your full name, by concatenating different parts of your name. Different cultures vary in the number of parts that names have (some have multiple middle names, some have no middle names at all), so you may end up concatenating additional strings or removing a string from the starter expression.

59 | "firstname" + "othername" + "lastname" 60 |

Call expressions

61 |

Many expressions use function calls that return values. For example, instead of writing 2 ** 100, we could opt to write pow(2, 100) and get the same result. The pow() function is one of Python’s many built-in functions, which means it can be used inside any Python program anywhere.

62 |

A few more handy built-ins:

63 |
max(50, 300)  # Results in 300
64 | 
65 | min(-1, -300) # Results in -300
66 | 
67 | abs(-100)     # Results in 100
68 |

In fact, every arithmetic expression from above can also be expressed using function calls, but not all of the functions are built-in. Instead, we must import the functions from the Python standard library.

69 |

So, to make a function call that adds two numbers, our program starts off with a line that imports the add function from the operator module:

70 |
from operator import add
71 | 
72 | add(18, 69)
73 |

Or similarly, for subtraction:

74 |
from operator import sub
75 | 
76 | sub(2021, 37)
77 |

Why would we go through the effort of using the function when we could just use the arithmetic operator? In most programs, we wouldn’t. But it’s helpful to realize that every operator in Python can actually be expressed as a function call, since we’ll encounter some situations in the future where we can only use function calls and not operators.

78 | 79 | 80 |
81 | ➡️ Next up: Nested Call Expressions 82 |
83 | 84 | {% endblock %} 85 | 86 | {% block footer_scripts %} 87 | {% include "_code-exercise-script.html" %} 88 | {% endblock %} -------------------------------------------------------------------------------- /content/1-python-fundamentals/10-exercise-conditionals.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: Conditionals{% endblock %} 3 | {% block content %} 4 |

Exercise: Conditionals

5 |

In this exercise, you'll define functions that consist entirely of compound conditionals (if/elif/else). Think carefully about the order of your conditions; sometimes order can have a big effect!

6 | 7 |
8 |

Lesser Num

9 |

📺 Need help getting started on this one? Watch me go through a similar problem in this video.

10 |

🧩 Or try a Parsons Puzzle version of the similar problem first.

11 | 12 | 13 | def lesser_num(num1, num2): 14 | """ Returns whichever number is lowest of the two supplied numbers. 15 | 16 | >>> lesser_num(45, 10) 17 | 10 18 | >>> lesser_num(-1, 30) 19 | -1 20 | >>> lesser_num(20, 20) 21 | 20 22 | """ 23 | # YOUR CODE HERE 24 | 25 | 26 |
27 |

Hello World

28 |

In this exercise, you'll use conditionals to translate a phrase into three different languages. Once you get the tests to pass, I encourage you to add another language you know!

29 |

🧩 For more guidance, try a Parsons Puzzle version of the problem first.

30 | def hello_world(language_code): 31 | """ Returns the translation of "Hello, World" into the language 32 | specified by the language code (e.g. "es", "pt", "en"), 33 | defaulting to English if an unknown language code is specified. 34 | >>> hello_world("en") 35 | 'Hello, World' 36 | >>> hello_world("es") 37 | 'Hola, Mundo' 38 | >>> hello_world("pt") 39 | 'Olá, Mundo' 40 | """ 41 | # YOUR CODE HERE 42 | 43 |
44 |

Grade Assigner

45 |

🧩 For more guidance, try a Parsons Puzzle version of the problem first.

46 | def assign_grade(score): 47 | """ Returns a letter grade for the numeric score based on the grade bins of: 48 | "A" (90-100), "B" (80-89), "C" (70-79), "D" (65-69), or "F" (0-64) 49 | 50 | >>> assign_grade(95) 51 | 'A' 52 | >>> assign_grade(90) 53 | 'A' 54 | >>> assign_grade(85) 55 | 'B' 56 | >>> assign_grade(80) 57 | 'B' 58 | >>> assign_grade(77) 59 | 'C' 60 | >>> assign_grade(70) 61 | 'C' 62 | >>> assign_grade(68) 63 | 'D' 64 | >>> assign_grade(65) 65 | 'D' 66 | >>> assign_grade(64) 67 | 'F' 68 | >>> assign_grade(0) 69 | 'F' 70 | """ 71 | return '' 72 | 73 |
74 | ➡️ Next up: User input 75 |
76 | {% endblock %} 77 | {% block footer_scripts %} 78 | {% include "_code-exercise-script.html" %} 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /content/1-python-fundamentals/11-user-input.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}User input{% endblock %} 3 | {% block content %} 4 |

User input

5 |

Many programs need a way to accept user input. In fact, one of my favorite parts of coding is making a program that accepts user input and then seeing what users pass into the program. You never know what users will say!

6 |

Python provides a built-in function for accepting input in command-line programs: input.

7 |

The input function takes a single optional argument, and displays that as the prompt. Here's what that looks like when used inside the Python console:

8 |

A Python REPL with input() command and user entering textOnce the user answer the prompts, the function returns the user's answer. The following code prompts a user and saves the answer into a variable:

9 |
answer = input('How are you? ')
10 |

We could then use that answer in future expressions, as the input to function calls, inside a Boolean expression, etc.

11 |

Play around with the input examples below. Notice that this editor pops up a prompt in the browser, so the interface for this is different than running the same code in a local Python console.

12 |
feeling = input('And how are you today?')
13 | print('I am also feeling ' + feeling)
14 | 
15 | ice_cream = input('Whats your fav ice cream?')
16 | print('Mmmm, ' + ice_cream + ' on a hot day sounds so good!')
17 | 18 | 19 |

Converting strings to numbers

20 |

The input function always returns the user input as a string. That’s all well and good if that’s what your code is expecting, but what if you are asking them a question with a numerical answer?

21 |

To convert a string to an integer, wrap it in int() and Python will attempt to construct an integer based on the string. For example:

22 |
int("33")  # 33
23 | int("0")   # 0
24 |

You can’t pass a string that looks like a floating point number to int() however. In that case, you need to use float().

25 |
float('33.33')
26 | float('33.0')
27 | float('33')
28 |

If you’re not sure whether a user’s input is going to include a decimal point, the safest approach is to use float().

29 |

If you want to remove the digits after the decimal afterwards, you can then use either int(), round(), floor(), or ceil().

30 |

Play around with the example below; try giving floating point answers and see what happens.

31 |
from math import floor, ceil
32 | 
33 | answer = input('How old is the toddler?')
34 | age = float(answer)
35 | 
36 | print('They are exactly ', age)
37 | print('They are approx ', round(age))
38 | print('They are older than ', floor(age))
39 | print('They are no older than ', ceil(age))
40 | 41 |
42 | ➡️ Next up: Project 1: Algorithms as Decision Makers 43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /content/1-python-fundamentals/12-project-1-algorithms-as-decision-makers.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Algorithms as Decision Makers{% endblock %} 3 | {% block content %} 4 |

Project 1: Algorithms as Decision Makers

5 |

Computer programs make decisions that affect human lives all the time. Banks use algorithms to decide who is eligible for a loan, universities use algorithms to decide which students get on-campus housing, Gmail uses algorithms to decide whether to send an email to the spam folder. Since the fate of individual's lives can depend on decisions made by algorithms, it's very important to think about how to make good decisions.

6 |

In this project, you'll build a program that can make a decision about how to prioritize an individual for an opportunity. You'll practice variables, functions, conditionals, and user input, all combined in one program. You'll also figure out how to test this program, both automatically and in the real world.

7 |

Instructions

8 |

You'll be developing this project using a Python notebook in Google CoLab. A Python notebook is a kind of document that combines text blocks and runnable code blocks. You can run each code block separately, or you can run all the code blocks in the notebook in sequence. When you run a code block, the output will be displayed below the block, and when you come back to a notebook after closing it, you'll see the previously generated output. All of the code in the notebook runs in the same Python environment, so a code block will have access to any names that have been assigned in previously run code.

9 |

Python notebooks are very popular with the data science and scientific programming community, since they are an easy way to see the steps of an analysis, but they're also generally helpful when learning Python.

10 |

To get started, make a copy of the Project 1 notebook for yourself. The rest of the instructions are in the notebook.

11 | 12 | 13 |
14 | ➡️ Next unit: Loops & lists 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /content/1-python-fundamentals/4-functions.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Functions{% endblock %} 3 | {% block content %} 4 |

Functions

5 |

A function is a sequence of code that performs a task and can be easily reused. ♻️

6 |

We've already used functions in call expressions like:

7 |
abs(-100)
  8 | 
  9 | pow(2, 10)
10 |

A function takes in an input (the arguments) and returns an output (the return value).

11 |

-100 → abs → 100

12 |

In the abs expression, there is a single argument of -100 and a return value of 100.

13 |

2, 10 → pow → 1024

14 |

In the pow expression, there are two arguments of 2 and 10, and a return value of 1024.

15 |

Those are all functions provided by Python, however. Many times, we want to create our own functions, to perform some task unique to our program.

16 | 17 | 18 |
19 |
20 |

🤔 Brainstorming Time

21 |
22 |
23 |

24 |

If you could create any function, what would your function be able to do? What would be the input/output?

25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 |
35 | 36 |

Defining functions

37 |

The most common way to define functions in Python is with a def statement.

38 |

Here's the general template:

39 |
def ():
 40 |     return 
41 |

Here's an actual example:

42 |
def add_two(num1, num2):
 43 |     return num1 + num2
44 |

Once defined, we can call the function, just like we call built-in Python functions:

45 |
add_two(2, 2)
 46 | add_two(18, 69)
47 |

Let's break down that function definition statement a little. The first line that declares the name and parameters is called the function signature, and all the indented lines below it are the function body.

48 |
def ():      # ← Function signature
 49 |     return  # ← Function body
50 |
def add_two(num1, num2):       # ← Function signature
 51 |     return num1 + num2         # ← Function body
52 |

The Python language is somewhat unique in that it uses indentation to show how lines of code relate to each other. (Other languages use brackets or keywords). That means that the lines in the function body must be indented. It doesn't actually matter how much they're indented - heck, Python will let you get away with indenting them with 200 spaces! However, the standard convention is either 2 spaces or 4 spaces; enough indentation to be visible to someone reading the code, but not so much that lines of code become ridiculously long.

53 |

The function body can (and often does) have multiple lines:

54 |
def add_two(num1, num2):      # ← Function signature
 55 |     result = num1 + num2      # ← Function body
 56 |     return result             # ← Function body
57 |

Parameters vs. arguments

58 |

We've now used two different terms to refer to the values that get passed into a function: parameters and arguments. Well, which is it?? Both!

59 |

When we're defining a function, the parameters are the names given to the values that will eventually get passed into a function. The following function accepts two parameters, num1 and num2, and the return value sums up those two parameters.

60 |
def add_two(num1, num2): 
 61 |     return num1 + num2 
62 |

However, when we're actually calling that function, we describe the values passed in as the arguments. Below, we pass in two arguments of 18 and 69.

63 |
add_two(18, 69)
64 |

The difference might seem subtle and overly pedantic, and well, as it turns out, programmers can be pedantic sometimes. 😊

65 |

Return values

66 |

The return keyword returns a value to whoever calls the function. After a return statement is executed, Python exits that function immediately, executing no further code in the function.

67 |
def add_two(num1, num2):
 68 |     return num1 + num2
 69 | 
 70 | result = add_two(2, 4)
71 |

After running that code, the variable result holds the return value, 6.

72 |

Just like we did with the built-in functions, we can use function calls in arithmetic expressions:

73 |
big_sum = add_two(200, 412) + add_two(312, 256)
74 |

And also nest calls inside other calls:

75 |
huge_sum = add(add(200, 412), add(312, 256))
76 |

Spot the bug! 🐞

77 |

There's something wrong with this code... do you see the problem?

78 |
def add_two(num1, num2):
 79 |     return result
 80 |     result = num1 + num2
 81 | 
 82 | result = add_two(2, 4)
83 | 84 | 85 | 86 |
87 |
88 |

🧠 Check Your Understanding

89 |
90 |
91 |

92 |

Where's the bug?

93 |
94 | 95 |
96 |
97 | 98 | 99 |
100 | 103 |
104 |
105 |
106 | 107 | 108 |

Spot the bug! 🐛

109 |

Here's some more buggy code:

110 |
def add_two():
111 |     return num1 + num2
112 | 
113 | result = add_two(2, 4)
114 | 115 | 116 | 117 |
118 |
119 |

🧠 Check Your Understanding

120 |
121 |
122 |

123 |

Where's the bug?

124 |
125 | 126 |
127 |
128 | 129 | 130 |
131 | 137 |
138 |
139 |
140 | 141 |

Spot the bug! 🦟

142 |

Here's the last bit of code that's problematic:

143 |
def add_two(num1, num2):
144 |     result = num1 + num2
145 | 
146 | result = add_two(2, 4)
147 | 148 | 149 | 150 |
151 |
152 |

🧠 Check Your Understanding

153 |
154 |
155 |

156 |

Where's the bug?

157 |
158 | 159 |
160 |
161 | 162 | 163 |
164 | 170 |
171 |
172 |
173 | 174 |
175 | ➡️ Next up: Exercise: Functions 176 |
177 | {% endblock %} 178 | 179 | {% block footer_scripts %} 180 | {% include "_quiz-js-include.html" %} 181 | {% endblock %} -------------------------------------------------------------------------------- /content/1-python-fundamentals/5-exercise-functions.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: Functions{% endblock %} 3 | {% block content %} 4 |

Exercise: Functions

5 |

Throughout this course, we will be writing many of our own functions. We want to make sure that our functions work correctly- that for any given input, they return the correct output.

6 |

One way to tests functions in Python is with doctests. A doctest shows a bunch of calls to a function and the expected output for each. The doctest for a function is always below the function header and before the function body, inside a giant comment known as the docstring.

7 |

For example:

8 |
def add_two(num1, num2):
  9 |     """ Adds two numbers together.
 10 | 
 11 |     >>> add_two(4, 8)
 12 |     12
 13 |     """
 14 |     return num1 + num2
15 |

Every function should have at least one example in the doctest, but it's better if it has many examples to test different input types and situations. For example, if our function deals with numbers, how does it deal with negative numbers? How about floating point numbers? Let's test!

16 |
def add_two(num1, num2):
 17 |     """ Adds two numbers together.
 18 | 
 19 |     >>> add_two(4, 8)
 20 |     12
 21 |     >>> add_two(-4, 8)
 22 |     4
 23 |     >>> add_two(1.1, 1.1)
 24 |     2.2
 25 |     """
 26 |     return num1 + num2
27 |

In the exercises, you should first look at the doctests as an additional way of making sure you understand the function you're about to write, and then once written, you should run the tests to make sure that your function works how the doctests expect.

28 | 29 |

Lifetime Supply Calculator

30 |

📺 If you'd like, you can watch how I tackled this first exercise in a walkthrough video.

31 |
32 | Or, if you'd like more guidance in written form, expand this block. 33 | 38 |
39 | For a code template, expand this block. 40 |

Fill in the blanks with the appropriate parameters and try running the tests.

41 |
return (100 - ______) * (______ * 365)
42 |
43 |
44 | 45 | def calculate_lifetime_supply(current_age, amount_per_day): 46 | """ Returns the amount of items consumed over a lifetime 47 | (with a max age of 100 assumed) based on the current age 48 | and the amount consumed per day. 49 | 50 | >>> calculate_lifetime_supply(99, 1) 51 | 365 52 | >>> calculate_lifetime_supply(99, 2) 53 | 730 54 | >>> calculate_lifetime_supply(36, 3) 55 | 70080 56 | """ 57 | # YOUR CODE HERE 58 | 59 |

Seconds Between

60 | 61 |

Implement a function, seconds_between, that returns the number of seconds between years, based on the assumption of 365 days in a year, 24 hours in a day, 60 minutes in an hour, and 60 seconds in a minute.

62 |
63 | If you'd like more guidance, expand this block. 64 | 69 |
70 | For a code template, expand this block. 71 |

Fill in the blank with the appropriate expression and try running the tests.

72 |
return (_____________) * 365 * 24 * 60 * 60
73 |
74 |
75 | def seconds_between(year1, year2): 76 | """ Returns the number of seconds between two years. 77 | 78 | >>> seconds_between(1984, 1985) 79 | 31536000 80 | >>> seconds_between(1984, 2000) 81 | 504576000 82 | """ 83 | # YOUR CODE HERE 84 | 85 |

Fortune Teller

86 |

Implement a function, tell_fortune, that returns a string of the form shown in the docstring and doctests. The string will be a combination of string literals (strings in quotes) and the given parameters.

87 | 88 |
89 | If you'd like more guidance, expand this block. 90 | 95 |
96 | For a code template, expand this block. 97 |

Fill in the blanks with the appropriate parameters and try running the tests.

98 |
return 'You will be a ' + ____ + ' living in ' + ____ + ' with ' + ____ + '.'
99 |
100 |
101 | def tell_fortune(job_title, location, partner): 102 | """ Returns a fortune of the form: 103 | 'You will be a <job_title> in <location> living with <partner>.' 104 | 105 | >>> tell_fortune('bball player', 'Spain', 'Sonic the Hedgehog') 106 | 'You will be a bball player in Spain living with Sonic the Hedgehog.' 107 | >>> tell_fortune('farmer', 'Kansas', 'C3PO') 108 | 'You will be a farmer in Kansas living with C3PO.' 109 | >>> tell_fortune('Elvis Impersonator', 'Russia', 'Karl the Fog') 110 | 'You will be a Elvis Impersonator in Russia living with Karl the Fog.' 111 | """ 112 | return "?" # YOUR CODE HERE 113 | 114 |

Temperature Converter

115 |

Implement two functions, celsius_to_fahrenheit and fahrenheit_to_celsius, to convert temperatures based on the standard conversion formulas. You'll need to modify both the function signature and function body.

116 |

Tip: As the final step in the conversion, the built-in round function may be helpful.

117 | def celsius_to_fahrenheit(): # <- COMPLETE THIS 118 | """ Returns the Fahrenheit equivalent of the given Celsius temperature. 119 | 120 | >>> celsius_to_fahrenheit(0) 121 | 32 122 | >>> celsius_to_fahrenheit(100) 123 | 212 124 | """ 125 | # YOUR CODE HERE 126 | 127 | def fahrenheit_to_celsius(): # <- COMPLETE THIS 128 | """ Returns the Celsius equivalent of the given Fahrenheit temperature. 129 | 130 | >>> fahrenheit_to_celsius(32) 131 | 0 132 | >>> fahrenheit_to_celsius(212) 133 | 100 134 | """ 135 | # YOUR CODE HERE 136 | 137 |
138 | ➡️ Next up: More on Functions 139 |
140 | {% endblock %} 141 | 142 | {% block footer_scripts %} 143 | {% include "_code-exercise-script.html" %} 144 | {% endblock %} 145 | -------------------------------------------------------------------------------- /content/1-python-fundamentals/8-exercise-logical-expressions.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: Logical expressions{% endblock %} 3 | {% block content %} 4 |

Exercise: Logical expressions

5 |

In this exercise, you'll define functions to return Boolean expressions based on input parameters. Before you start coding, remember to check the doctests for examples of what should return True/False. When you think you're done coding, run the tests to make sure they all pass.

6 | 7 |

Curly Hair Checker

8 |

📺 Need help getting started on this one? Watch me go through a similar problem in this video.

9 | 10 |
11 | Or, if you'd like more guidance in written form, expand this block. 12 | 17 |
18 | For a code template, expand this block. 19 |

Fill in the blanks with the correct comparison and try running the tests.

20 |
return father_allele ______ and mother_allele ______
21 |
22 |
23 | 24 | def has_curly_hair(father_allele, mother_allele): 25 | """Returns true only if both the father allele and mother allele are "C". 26 | 27 | >>> has_curly_hair("C", "c") 28 | False 29 | >>> has_curly_hair("c", "C") 30 | False 31 | >>> has_curly_hair("s", "s") 32 | False 33 | >>> has_curly_hair("C", "C") 34 | True 35 | """ 36 | return 0 37 | 38 |

Presidential Eligibility

39 | 40 |
41 | If you'd like more guidance, expand this block. 42 | 47 |
48 | For a code template, expand this block. 49 |

Fill in the blanks with the correct comparison and try running the tests.

50 |
return ______ and ______
51 |
52 |
53 | 54 | def can_be_president(age, residency): 55 | """ 56 | Returns whether someone can be US president based on age and residency. 57 | According to the US constitution, a presidential candidate must be at least 58 | 35 years old and have been a US resident for at least 14 years. 59 | 60 | >>> can_be_president(30, 10) 61 | False 62 | >>> can_be_president(36, 10) 63 | False 64 | >>> can_be_president(30, 16) 65 | False 66 | >>> can_be_president(36, 15) 67 | True 68 | >>> can_be_president(36, 14) 69 | True 70 | >>> can_be_president(35, 14) 71 | True 72 | >>> can_be_president(35, 30) 73 | True 74 | """ 75 | return 0 # REPLACE THIS 76 | 77 |

Seafood Safety

78 | 79 |
80 | If you'd like more guidance, expand this block. 81 | 86 |
87 | For a code template, expand this block. 88 |

Fill in the blanks with the correct comparison and try running the tests.

89 |
return ______ or ______
90 |
91 |
92 | 93 | def is_safe_to_eat(seafood_type, days_frozen): 94 | """ Returns true if the seafood is safe to eat: 95 | either the type is "mollusk" or it's been frozen for at least 7 days. 96 | 97 | >>> is_safe_to_eat("tuna", 3) 98 | False 99 | >>> is_safe_to_eat("salmon", 6) 100 | False 101 | >>> is_safe_to_eat("salmon", 7) 102 | True 103 | >>> is_safe_to_eat("mollusk", 1) 104 | True 105 | >>> is_safe_to_eat("mollusk", 9) 106 | True 107 | """ 108 | return 0 # REPLACE THIS 109 | 110 |

Harvest Time

111 | def harvest_time(month, tuber_size): 112 | """ Returns true if it's time to harvest root vegetables: 113 | either the month is July or the size of the tuber is at least 2 feet. 114 | >>> harvest_time("May", 1) 115 | False 116 | >>> harvest_time("May", 2) 117 | True 118 | >>> harvest_time("May", 3) 119 | True 120 | >>> harvest_time("July", 1) 121 | True 122 | >>> harvest_time("July", 2) 123 | True 124 | >>> harvest_time("July", 4) 125 | True 126 | """ 127 | return 0 # REPLACE THIS 128 | 129 |
130 | ➡️ Next up: Conditionals 131 |
132 | {% endblock %} 133 | {% block footer_scripts %} 134 | {% include "_code-exercise-script.html" %} 135 | {% endblock %} 136 | -------------------------------------------------------------------------------- /content/1-python-fundamentals/9-conditionals.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Conditionals{% endblock %} 3 | {% block content %} 4 |

Conditionals

5 |

Now that we know how to write Boolean expressions, we can use them inside conditional statements to tell our programs to conditionally execute one block of code versus another.

6 |

Conditional statements

7 |

In Python, conditional statements always start with if. Here's the basic template:

8 |
if <condition>:
  9 |     <statement>
 10 |     <statement>
 11 |     ...
12 |

If <condition> is true, then the program will execute the statements indented below it. The indentation of the inner statements is required in Python, and indenting incorrectly will result in either a syntax error or a buggy program.

13 |

Here's a simple if statement that re-assigns a variable based on a condition:

14 |
clothing = "shirt"
 15 | 
 16 | if temperature < 32:
 17 |     clothing = "jacket"
18 | 19 | 20 | 21 |
22 |
23 |

🧠 Check Your Understanding

24 |
25 |
26 |
27 |

In which situations will clothing be assigned to "jacket"?

28 |
29 |
30 | 37 | 40 | 43 |
44 |
45 | 52 | 55 | 58 |
59 |
60 | 67 | 70 | 73 |
74 |
75 | 82 | 85 | 88 |
89 |
90 | 91 | 92 |
93 |
94 |
95 |
96 | 97 |

Compound conditionals

98 |

A conditional statement can include any number of elif statements to check other conditions and execute different blocks of code. Other languages use else if, but Python decided that was just too dang long and shortened it to elif. 🤷🏼‍♀️

99 |

Here's a template with three conditional paths:

100 |
if >condition<:
101 |     <statement>
102 |     ...
103 | elif <condition>:
104 |     <statement>
105 |     ...
106 | elif <condition>:
107 |     <statement>
108 |     ...
109 |

Here's an extension of the earlier example with one more path:

110 |
clothing = "shirt"
111 | 
112 | if temperature < 0:
113 |     clothing = "snowsuit"
114 | elif temperature < 32:
115 |     clothing = "jacket"
116 |

One way to make sure we understand a conditional is to make a table showing possible values of program state and the result.

117 |

For example:

118 | 119 | 120 | 121 | 126 | 127 | 132 | 137 | 142 | 147 | 152 | 157 |
122 |

temperature

123 |
124 |

clothing

125 |
128 |

-50

129 |
130 |

"snowsuit"

131 |
133 |

-1

134 |
135 |

"snowsuit"

136 |
138 |

0

139 |
140 |

"jacket"

141 |
143 |

1

144 |
145 |

"jacket"

146 |
148 |

31

149 |
150 |

"jacket"

151 |
153 |

32

154 |
155 |

"shirt"

156 |
158 |

50

159 |
160 |

"shirt"

161 |
162 |

It's a good idea to be doubly sure of what happens at the boundary conditions- sometimes you might realize, "uh oh! I wanted >=, not >, my bad!". 🙀When writing tests for a function that uses a conditional function, make sure to test those boundary conditions.

163 |

Finally, a conditional statement can include an else clause to specify what code should be executed if none of the preceding conditions were true.

164 |
if <condition>:
165 |     <statement>
166 |     ...
167 | elif <condition>:
168 |     <statement>
169 |     ...
170 | else <condition>:
171 |     <statement>
172 |     ...
173 |

Here's a way to rewrite the earlier example using if/elif/else:

174 |
if temperature < 0:
175 |     clothing = "snowsuit"
176 | elif temperature < 32:
177 |     clothing = "jacket"
178 | else:
179 |     clothing = "shirt"
180 |

That code would result in the same values for clothing as shown in the table below, but reads more clearly.

181 |

In summary, a conditional statement:

182 | 185 |

Using conditionals in functions

186 |

A conditional statement could be just one part of a longer function, or it could be the entire function. It's common to write a function that uses a conditional to compare the input parameters and return values based on what was passed in.

187 |

For example:

188 |
def get_number_sign(num):
189 |     if num < 0:
190 |         sign = "negative"
191 |     elif num > 0:
192 |         sign = "positive"
193 |     else:
194 |         sign = "neutral"
195 |     return sign
196 |

We'd call that function like this:

197 |
get_number_sign(50)  # Returns: "positive"
198 | get_number_sign(-1)  # Returns: "negative"
199 | get_number_sign(0)   # Returns: "neutral"
200 |

We could also end each branch of the conditional with a return, rewriting the above function like so:

201 |
def get_number_sign(num):
202 |     if num < 0:
203 |         return "negative"
204 |     elif num > 0:
205 |         return "positive"
206 |     else:
207 |         return "neutral"
208 |

That makes it very clear that the function won't be doing anything else besides returning that value, since a return statement exits a function immediately.

209 |

There's one more way we might write this function, leaving off the else of the conditional:

210 |
def get_number_sign(num):
211 |     if num < 0:
212 |         return "negative"
213 |     elif num > 0:
214 |         return "positive"
215 |     return "neutral"
216 |

When that function receives a num of 0, it doesn't execute either of the return statements inside the conditional, since neither condition is true. It continues on, discovers the next statement is a return statement, and executes that statement instead.

217 | 218 |
219 | ➡️ Next up: Exercise: Conditionals 220 |
221 | {% endblock %} 222 | 223 | {% block footer_scripts %} 224 | {% include "_quiz-js-include.html" %} 225 | {% endblock %} -------------------------------------------------------------------------------- /content/2-loops-&-lists/0-unit-overview.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Unit 2: Loops & Lists{% endblock %} 3 | {% block content %} 4 |

Unit 2: Loops & Lists

5 |

Welcome to the second unit of this course! Don't worry if your mind is a little bit blown, learning programming can be like that. 🤯 Just keep at it, and reach out to the community when you need help.

6 |

Our learning goals for this unit are:

7 | 12 |

At the end, you'll use your newfound knowledge to build a bunch of photo filters, like grayscale and inversion, and apply them to whatever photo you'd like.

13 |
14 | ➡️ Next up: While Loops 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /content/2-loops-&-lists/10-exercise-nested-lists.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Nested lists{% endblock %} 3 | {% block content %} 4 |

Exercise: Nested lists

5 |

Try out iterating through nested lists and computing different things in the exercises below. These are a great practice for this unit's project, as it will involve a lot of nested lists.

6 |

Sum Grid

7 |

Implement sum_grid, a function that returns the sum of all the numbers in grid, a 2-dimensional list of numbers.

8 |

📺 Need help getting started? Here's a video walkthrough of a similar problem.

9 |

🧩 Or, for more guidance in written form, try a Parsons Puzzle version first.

10 | def sum_grid(grid): 11 | """Returns the sum of all the numbers in the given grid, a 2-D list of numbers. 12 | 13 | >>> grid1 = [ 14 | ... [5, 1, 6], # 12 15 | ... [10, 4, 1], # 15 16 | ... [8, 3, 2] # 13 17 | ... ] 18 | >>> sum_grid(grid1) 19 | 40 20 | >>> grid2 = [ 21 | ... [15, 1, 6], # 22 22 | ... [10, 4, 0], # 14 23 | ... [8, 3, 2] # 13 24 | ... ] 25 | >>> sum_grid(grid2) 26 | 49 27 | """ 28 | # Initialize a sum to 0 29 | # Iterate through the grid 30 | # Iterate through current row 31 | # Add current value to sum 32 | # Return sum 33 | 34 |

Contains 15 row?

35 |

Implement contains_15row, a function that returns true if any of the rows in grid, a 2-dimension list of numbers, add up to a total of 15.

36 |

🧩 For more guidance, try a Parsons Puzzle version first.

37 | def contains_15row(grid): 38 | """Takes an input of a 2-dimensional list of numbers 39 | and returns true if any of the rows add up to 15. 40 | 41 | >>> grid1 = [ 42 | ... [5, 1, 6], # 12 43 | ... [10, 4, 1], # 15!! 44 | ... [8, 3, 2] # 13 45 | ... ] 46 | >>> contains_15row(grid1) 47 | True 48 | >>> grid2 = [ 49 | ... [15, 1, 6], # 22 50 | ... [10, 4, 0], # 14 51 | ... [8, 3, 2] # 13 52 | ... ] 53 | >>> contains_15row(grid2) 54 | False 55 | """ 56 | # Iterate through the grid 57 | # Initialize a sum to 0 58 | # Iterate through the row 59 | # Add current value to sum 60 | # If sum is 15, return true 61 | # return false otherwise 62 | 63 |
64 | ➡️ Next up: Project 2: Photo filters 65 |
66 | {% endblock %} 67 | {% block footer_scripts %} 68 | {% include "_code-exercise-script.html" %} 69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /content/2-loops-&-lists/11-project-2-photo-filters.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Project 2: Photo Filters{% endblock %} 3 | {% block content %} 4 |

Project 2: Photo Filters

5 |

In this project, you'll be applying filters to photos, like inversion, flipping, and grayscale- and you'll be writing all those filters yourself!

6 |

A photo can be represented in computer memory as a nested list of pixels, so playing with photos is a great way to practice your newfound list knowledge. And, of course, in order to process a nested list, you'll need nested loops, so photo filters involve lots of nested loops. You'll also learn a bit about representing pixels in the RGB color space, if you've never played with RGB before.

7 |

One of the reasons that I love playing with graphics when I'm programming is that even the bugs are fun - weird colors, all black images, all sorts of shenanigans. So get ready for some colorful bugs and a lot of learning! :)

8 |

Instructions

9 |

Once again, you'll be using a Python notebook on Google CoLab to develop this project.

10 |

To get started, make a copy of the Project 2 notebook for yourself. The rest of the instructions are in the notebook.

11 | 12 |
13 | ➡️ Next unit: Strings & dictionaries 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /content/2-loops-&-lists/2-exercise-while-loops.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: While loops{% endblock %} 3 | {% block content %} 4 |

Exercise: While loops

5 |

Practice while loops in these exercises, and run the doctests when you think you've got your code working. Remember, it's very easy (and common, even for experienced engineers) to code off-by-one errors, so keep your eyes peeled for those. 👀 Also, if you accidentally write an infinite loop, you will very likely crash the browser tab. If that happens, congratulations, that's a programmer rite of passage! Just reload the page and try to write a finite loop instead.

6 |

Count Evens

7 |

Implement count_evens, a function that counts the number of even numbers between a given start number and a given end number. The count should include the start or end numbers if they are also even.

8 |

Tip: To check if a number is even, use the % operator, which reports the remainder after dividing one number by another number. An even number divided by 2 has a remainder of 0.

9 |

📺 You can watch me walk through this problem in a video. If you don't want to see the solution, make sure to pause when you feel like you've seen enough to get started.

10 |

🧩 Or, for more guidance in written form, try a Parsons Puzzle version first.

11 | 12 | def count_evens(start, end): 13 | """ 14 | Counts the number of even numbers between a given start number and given end number. 15 | Count should include the start or end if they are even. 16 | 17 | >>> count_evens(2, 2) 18 | 1 19 | >>> count_evens(-2, 52) 20 | 28 21 | >>> count_evens(237, 500) 22 | 132 23 | """ 24 | # YOUR CODE HERE 25 |

Count Multiples

26 |

Implement count_multiples, a function that counts how many multiples of a given divisor are between the start and end numbers. It should include the start or end if they are also a multiple of divisor.

27 |

Tip: The % operator is once again helpful here, for determining if one number is a multiple of another. In fact, you may want to start from your previous code and see how little you need to change to get this function implemented correctly.

28 |

🧩 For more, try a Parsons Puzzle version first.

29 | 30 | def count_multiples(start, end, divisor): 31 | """ 32 | Counts the number of multiples of divisor between the start and end numbers. 33 | It should include the start or end if they are a multiple. 34 | 35 | >>> count_multiples(2, 2, 1) # 2 is a multiple of 1 36 | 1 37 | >>> count_multiples(2, 2, 2) # 2 is a multiple of 2 38 | 1 39 | >>> count_multiples(2, 2, 3) # 2 is not a multiple of 3 40 | 0 41 | >>> count_multiples(1, 12, 3) # 3, 6, 9, 12 42 | 4 43 | >>> count_multiples(237, 500, 10) 44 | 27 45 | """ 46 | # YOUR CODE HERE 47 | 48 |

Sum Multiples

49 |

Implement sum_multiples, a function that returns the sum of the multiples of a given divisor between the given start and end numbers. If the start or end numbers are also multiples, they should also be included in the sum.

50 |

Hint: The main difference between this exercise and the previous exercise is summing versus counting. What do you need to change from your previous code to make the return value a sum instead?

51 | def sum_multiples(start, end, divisor): 52 | """Returns the sum of the multiples of a given divisor between start and end. 53 | If the start or end numbers are multiples, they should be included in the sum. 54 | 55 | >>> sum_multiples(1, 12, 4) 56 | 24 57 | >>> sum_multiples(1, 12, 13) 58 | 0 59 | >>> sum_multiples(2, 2, 2) 60 | 2 61 | >>> sum_multiples(2, 2, 3) 62 | 0 63 | >>> sum_multiples(23, 81, 13) 64 | 260 65 | """ 66 | # YOUR CODE HERE 67 |

Debug this! 🐞🐛🐜

68 |

The code for the next function is already written, but it has several bugs.

69 |

Tips for debugging:

70 | 75 | def product_of_numbers(end): 76 | """Returns the product of all the numbers from 1 to the end number, 77 | including the end number. 78 | 79 | >>> product_of_numbers(1) 80 | 1 81 | >>> product_of_numbers(2) 82 | 2 83 | >>> product_of_numbers(3) 84 | 6 85 | >>> product_of_numbers(10) 86 | 3628800 87 | """ 88 | result = 1 89 | counter = 0 90 | while counter < end: 91 | result *= counter 92 | counter += 2 93 | return result 94 |
95 | ➡️ Next up: For Loops & Ranges 96 |
97 | {% endblock %} 98 | {% block footer_scripts %} 99 | {% include "_code-exercise-script.html" %} 100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /content/2-loops-&-lists/5-looping-through-sequences.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Looping through sequences{% endblock %} 3 | {% block content %} 4 |

Looping through sequences

5 |

A sequence is any type of data that has sequential data inside it. A list is considered a sequence since it contains items in a sequence. A string is also considered a sequence since it contains characters in a sequence. The Python language also offers other sequential data types like tuples and sets, but we won't be diving into those in this course.

6 |

All sequence types can be looped through using a loop, either a while loop or for loop.

7 |

While loops with lists

8 |

It's very common to use a loop to process the items in a list, doing some sort of computation on each item.

9 |

For example, this while loop compares the counter variable i to the list length, and also uses that counter variable i as the current index:

10 |
scores = [80, 95, 78, 92]
11 | total = 0
12 | i = 0
13 | while i < len(scores):
14 |     score = scores[i]
15 |     total += score
16 |     i += 1
17 |

That loop sums up all the numbers in a list, storing the result in total, and stopping after it accesses the last element. Remember, the index of the last element is len(list) - 1), not len(list). That's why the comparison operator in the loop condition is < and not <=. Accidentally using <= instead will lead to the dreaded off-by-one error. 🙀(At this point, you might be beginning to see why off-by-one errors are so common in programming.)

18 | 19 |

For loops with lists

20 |

There's a much simpler way to iterate through a list: the for loop.

21 |
scores = [80, 95, 78, 92]
22 | total = 0
23 | for score in scores:
24 |     total += score
25 |

No counter variable, loop condition, or bracket notation needed! Python will start from the beginning of the list and put the current item in the score loop variable in each iteration, so your loop body can just reference score.

26 |

Looping through strings

27 |

Since a string is also a sequential data type, we can also use loops to iterate through a string. Looping over strings is less common than looping over lists, however.

28 |

The following loop prints out an integer representing the Unicode code point of each letter in a string:

29 |
letters = "ABC"
30 | for letter in letters:
31 |     print(ord(letter))
32 |

If we iterate through a string with characters that require multiple code points, there will be an iteration for each of those code points.

33 |
emoji = "🤷🏽‍♀️"
34 | for code_point in emoji:
35 |     print(ord(code_point))
36 |

How many numbers do you think that will print out? Try it and see!

37 | 38 | # Try some code here 39 | 40 |
41 | ➡️ Next up: Exercise: Loops and Lists 42 |
43 | {% endblock %} 44 | 45 | {% block footer_scripts %} 46 | {% include "_code-exercise-script.html" %} 47 | {% endblock %} -------------------------------------------------------------------------------- /content/2-loops-&-lists/6-exercise-loops-and-lists.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Loops and lists{% endblock %} 3 | {% block content %} 4 |

Exercise: Loops and lists

5 |

Practice lists and loops in this set of exercises. A few of them have comments to guide you towards a solution; you can follow those comments if they help or delete them and start from scratch. Whatever works for you!

6 |

Make point

7 |

Implement make_point, a function that returns a list with the two arguments x and y, each rounded to integer values.

8 |

Tip: Remember one of Python's built-in functions makes it easy to round numbers.

9 |

📺 Need help getting started? Watch me go through a similar problem in this video.

10 | def make_point(x, y): 11 | """Returns a list with the two arguments, rounded to their integer values. 12 | 13 | >>> make_point(30, 75) 14 | [30, 75] 15 | >>> make_point(-1, -2) 16 | [-1, -2] 17 | >>> make_point(12.32, 74.11) 18 | [12, 74] 19 | """ 20 | return [] 21 |

Average scores

22 |

Implement average_scores, a function that returns the average of the provided list scores.

23 |

📺 Need help getting started? Watch me go through a similar problem in this video.

24 |

🧩 Or, for more guidance in written form, try a Parsons Puzzle version first.

25 | def average_scores(scores): 26 | """Returns the average of the provided scores. 27 | 28 | >>> average_scores([10, 20]) 29 | 15.0 30 | >>> average_scores([90, 80, 70]) 31 | 80.0 32 | >>> average_scores([8.9, 7.2]) 33 | 8.05 34 | """ 35 | # Initialize a variable to 0 36 | # Iterate through the scores 37 | # Add each score to the variable 38 | # Divide the variable by the number of scores 39 | # Return the result 40 |

Concatenator

41 |

Implement concatenator, a function that returns a string made up of all the strings from the provided list items.

42 |

(There is a way to do this with a single function call in Python, but for the purposes of this exercise, please try using a for loop to join the strings.)

43 |

🧩 For more guidance, try a Parsons Puzzle version first.

44 | def concatenator(items): 45 | """Returns a string with all the contents of the list concatenated together. 46 | 47 | >>> concatenator(["Super", "cali", "fragilistic", "expialidocious"]) 48 | 'Supercalifragilisticexpialidocious' 49 | >>> concatenator(["finger", "spitzen", "gefühl"]) 50 | 'fingerspitzengefühl' 51 | >>> concatenator(["none", "the", "less"]) 52 | 'nonetheless' 53 | """ 54 | # Initialize a variable to empty string 55 | # Iterate through the list 56 | # Concatenate each item to the variable 57 | # Return the variable 58 | 59 |
60 | ➡️ Next up: List mutation 61 |
62 | {% endblock %} 63 | {% block footer_scripts %} 64 | {% include "_code-exercise-script.html" %} 65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /content/2-loops-&-lists/7b-exercise-list-mutation.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: List mutation{% endblock %} 3 | {% block content %} 4 |

Exercise: List mutation

5 |

In these exercises, you'll practice list mutation using both list methods and bracket notation.

6 |

Add to sequence

7 |

Implement add_to_sequence, a function that adds a final item to the end of a list that is 1 more than the previous final item of the list.

8 | 9 |
10 | If you'd like more guidance, expand this block. 11 | 16 |
17 | For a code template, expand this block. 18 |
last_num = sequence[__]
19 | sequence.______(last_num + 1)
20 |
21 |
22 | 23 | def add_to_sequence(sequence): 24 | """ 25 | >>> nums = [1, 2, 3] 26 | >>> add_to_sequence(nums) 27 | >>> nums 28 | [1, 2, 3, 4] 29 | >>> nums2 = [26, 27] 30 | >>> add_to_sequence(nums2) 31 | >>> nums2 32 | [26, 27, 28] 33 | """ 34 | 35 |

Malcolm in the Middle

36 |

Implement malcolm_in_middle, a function that adds a new element with the string 'malcolm' in the middle of the provided list. In the case of a list with odd elements, the string should be inserted to the "right" of the middle. See doctests for examples.

37 | 38 | 39 |
40 | If you'd like more guidance, expand this block. 41 | 46 |
47 | For a code template, expand this block. 48 |
sequence.________(___________, 'malcolm')
49 |
50 |
51 | 52 | def malcolm_in_middle(sequence): 53 | """ 54 | >>> kids = ['jamie', 'dewey', 'reese', 'francis'] 55 | >>> malcolm_in_middle(kids) 56 | >>> kids 57 | ['jamie', 'dewey', 'malcolm', 'reese', 'francis'] 58 | >>> kids2 = ['hunter', 'pamela', 'oliver'] 59 | >>> malcolm_in_middle(kids2) 60 | >>> kids2 61 | ['hunter', 'pamela', 'malcolm', 'oliver'] 62 | """ 63 | 64 |

Double time

65 |

Implement double_time, a function that doubles each element in the provided list.

66 |

Tip: Remember there are several ways to loops through lists. Which way will work better here?

67 |

🧩 For more guidance, try a Parsons Puzzle version first.

68 | def double_time(sequence): 69 | """ 70 | >>> nums = [1, 2, 3] 71 | >>> double_time(nums) 72 | >>> nums 73 | [2, 4, 6] 74 | >>> nums2 = [-1, -4] 75 | >>> double_time(nums2) 76 | >>> nums2 77 | [-2, -8] 78 | """ 79 | 80 |
81 | ➡️ Next up: More on lists 82 |
83 | {% endblock %} 84 | {% block footer_scripts %} 85 | {% include "_code-exercise-script.html" %} 86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /content/2-loops-&-lists/8-more-on-lists.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}More on lists{% endblock %} 3 | {% block content %} 4 |

More on lists

5 |

More list methods

6 |

Here are a few more helpful methods that can be called on lists:

7 | 8 | 9 | 10 | 15 | 16 | 21 | 26 |
11 |

Method

12 |
13 |

Description

14 |
17 |

reverse()

18 |
19 |

Reverses the items of the list, mutating the original list.

20 |
22 |

sort()

23 |
24 |

Sorts the items of the list, mutating the original list.

25 |
27 |

count(item)

28 |
29 |

Returns the number of occurrences of item in the list. If item is not found, returns 0.

30 |
31 |

These are documented in further details in that Python list documentation.

32 |

Starting from this list:

33 |
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
34 |

We can call reverse() on it like so - remember, it's a method of lists, so we write it using dot notation:

35 |
nums.reverse()
36 |

That method mutates the original list, so nums now holds [10, 9, 8, 7, 6, 5, 4, 3, 2, 1].

37 |

Now let's try out sorting, starting from this list:

38 |
scores = [9.5, 8.2, 7.3, 10.0, 9.2]
39 |

We can call the sort() method on it like so:

40 |
scores.sort()
41 |

That mutates the scores list, so it now holds [7.3, 8.2, 9.2, 9.5, 10.0]. If you wanted that sorted in the other direction, you could call reverse on it after, or just set the optional argument reverse.

42 |
scores.sort(reverse=True)
43 |

List functions

44 |

Some of the Python global built-in functions can be called on lists (or any iterable object, like strings). Since these are functions, not methods, we pass the list in as an argument instead of using dot notation to call it after the list name.

45 | 46 | 47 | 48 | 53 | 54 | 59 | 64 |
49 |

Function

50 |
51 |

Description

52 |
55 |

sorted(iterable, reverse=False)

56 |
57 |

Returns a sorted version of the iterable object. Set the reverse argument to True to reverse the results.

58 |
60 |

min(iterable)

61 |
62 |

Returns the minimum value in the iterable object.

63 |
65 |

max(iterable)

66 |
67 |

Returns the maximum value in the iterable object.

68 |
69 |

So, if you wanted a sorted version of a list but you didn't want to change the original list, you could call sorted() instead:

70 |
scores = [9.5, 8.2, 7.3, 10.0, 9.2]
71 | scores2 = sorted(scores)
72 |

You could also get the minimum and maximum values of that list:

73 |
max_score = max(scores)
74 | min_score = min(scores)
75 |

These three functions also take additional arguments to customize their behavior further, which you can learn more about in the documentation.

76 |

List containment

77 |

When processing lists with unknown data, we often want to check if a value is inside a list. We can do that using the in operator, which evaluates to True if a value is in a list.

78 |

For example:

79 |
groceries = ["apples", "bananas"]
80 | 
81 | if "bananas" in groceries:
82 |     print("⚠️ Watch your step!")
83 | 
84 | if "bleach" in groceries:
85 |     print("☣️ Watch what you eat!")
86 | 87 |
88 | ➡️ Next up: Nested lists 89 |
90 | {% endblock %} -------------------------------------------------------------------------------- /content/2-loops-&-lists/9-nested-lists.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Nested lists{% endblock %} 3 | {% block content %} 4 |

Nested lists

5 |

A Python list can contain any value, so in fact, the items can themselves be lists. When a list is made up of other lists, we call it a nested list.

6 |

Here's a nested list representing the names and scores of three gymnasts:

7 |
gymnasts = [
  8 |              ["Brittany", 9.15, 9.4, 9.3, 9.2],
  9 |              ["Lea", 9, 8.8, 9.1, 9.5],
 10 |              ["Maya", 9.2, 8.7, 9.2, 8.8]
 11 |            ]
12 |

We can treat this like any other list, using bracket notation and list methods to interact with the list.

13 | 14 |
15 |
16 |

🧠 Check Your Understanding

17 |
18 |
19 |
20 |

What's the length of gymnasts?

21 |
22 |
23 | 24 |
25 | 28 | 31 |
32 | 33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 |

What's the length of gymnasts[0]?

45 |
46 |
47 | 48 |
49 | 52 | 55 |
56 | 57 | 58 |
59 |
60 |
61 |
62 | 63 | 64 |

Accessing nested list items

65 |

Starting again with this nested list:

66 |
gymnasts = [
 67 |                 ["Brittany", 9.15, 9.4, 9.3, 9.2],
 68 |                 ["Lea", 9, 8.8, 9.1, 9.5],
 69 |                 ["Maya", 9.2, 8.7, 9.2, 8.8]
 70 |             ]
71 |

To access each of the three lists in gymnasts, we use the same bracket notation from before:

72 |
gymnasts[0]     # ["Brittany", 9.15, 9.4, 9.3, 9.2]
 73 | gymnasts[1]     # ["Lea", 9, 8.8, 9.1, 9.5],
 74 | gymnasts[2]     # ["Maya", 9.2, 8.7, 9.2, 8.8]
75 |

We can also reach deep into the nested list and access items inside the inner lists, however. We just add another level of bracket notation!

76 |
gymnasts[0][0]  # "Brittany"
 77 | gymnasts[1][0]  # "Lea"
 78 | gymnasts[1][4]  # 9.5
 79 | gymnasts[1][5]  # 🚫 IndexError!
 80 | gymnasts[3][0]  # 🚫 IndexError!
81 | 82 | 83 | 84 |
85 |
86 |

🧠 Check Your Understanding

87 |
88 |
89 |

Using bracket notation, how would you access the final score of 8.8?

90 |
91 | 97 |
98 | 101 | 104 |
105 | 106 | 107 |
108 |
109 |
110 |
111 | 112 |

Modifying nested lists

113 |

We can replace the inner lists inside a nested list using bracket notation.

114 |

This code replaces the entire first list with a brand new list:

115 |
gymnasts[0] = ["Olivia", 8.75, 9.1, 9.6, 9.8]
116 |

We can also modify items inside nested lists, using either bracket notation or list methods.

117 |
gymnasts[1][0] = "Leah"
118 | gymnasts[2][4] = 9.8
119 | gymnasts[2].append(10.5)
120 |

None of this is new functionality; this is the same functionality you learnt from before. It all just works as a consequence of Python allowing lists to contain other lists. I like to explicitly point it out, however, since it's not always immediately obvious, and nested lists are such a powerful data structure in programming.

121 |

Looping through nested lists

122 |

To iterate through every item in a nested list, we can use a nested for loop: a for loop inside a for loop!

123 |

Starting from this nested list of just the numeric scores:

124 |
team_scores = [
125 |                 [9.15, 9.4, 9.3, 9.2],
126 |                 [9, 8.8, 9.1, 9.5],
127 |                 [9.2, 8.7, 9.2, 8.8]
128 |               ]
129 |

We can sum them up using this nested for loop:

130 |
sum_scores = 0
131 | for member_scores in team_scores:
132 |     for score in member_scores:
133 |         sum_scores += score
134 |

The trickiest thing with nested loops is keeping track of what each loop variable represents. In the above code, member_scores stores each of the inner lists in each iteration- first [9.15, 9.4, 9.3, 9.2], then [9, 8.8, 9.1, 9.5], finally [9.2, 8.7, 9.2, 8.8]. Then in the inner loop, score stores each of the numbers inside those lists, starting with 9.15 and ending with 8.8.

135 |

If you're ever not sure what your variable is storing, you can always print it out and double check.

136 | 137 |
138 | ➡️ Next up: Exercise: Nested lists 139 |
140 | {% endblock %} 141 | 142 | {% block footer_scripts %} 143 | {% include "_quiz-js-include.html" %} 144 | {% endblock %} -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/0-unit-overview.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Unit 3: Strings & Dictionaries{% endblock %} 3 | {% block content %} 4 |

Unit 3: Strings & Dictionaries

5 |

It's your third unit of learning Python! We're going to be covering some of my personal favorite topics in this unit.

6 |

Our learning goals for this unit are:

7 | 11 |

At the end, you'll use those skills to build a procedural text generator that can output novel sentences from a book, movie titles, wise sayings, or whatever you decide.

12 | 13 |
14 | ➡️ Next up: String formatting 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/1-string-formatting.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}String formatting{% endblock %} 3 | {% block content %} 4 |

String formatting

5 |

Let's start off with a reminder of what we already know about the string data type in Python:

6 |

To create a string value, we wrap characters inside quotes, either double quotes or single quotes. Either is fine, as long as we're consistent with what we use for each string.

7 |
singer = 'Janelle Monáe'
 8 | play_song("Older", "Ben Platt")
9 |

To concatenate two strings together, we use the + operator. That works as long as both operands are strings, even if one of them is a variable containing a string.

10 |
lyric = "If you're happy and you know it, " + "clap your hands!"
11 | start = "If you're happy and you know it, "
12 | lyric1 = start + "clap your hands!"
13 | lyric2 = start + "wiggle your toes!"
14 | lyric3 = start + "hug a friend!"
15 |

Escape sequences

16 |

If the string contains the same quote character that is being used to surround the string, then that quote character must be escaped. That means we must use a special character to let Python know to not treat the quote character normally (since treating it normally would end the string).

17 |

This string is broken and results in a syntax error:

18 |
greeting = 'how're you doing?'
19 |

To fix it, insert a backslash right before the single quote:

20 |
greeting = 'how\'re you doing?'
21 |

The backslash is what is known as the escape character, and is used as the escape character in many languages. That means if you do want an actual backslash in a string, you need to escape the backslash with yet another backslash:

22 |
path = "C:\\My Documents"
23 |

There are also a few special escape sequences that produce something other than a standard character. The most useful sequence is "\n", which creates a new line in the string once it's printed out.

24 |

Here's a string storing a famous haiku by Matsuo Bashō:

25 |
haiku = "An old silent pond\nA frog jumps into the pond—\nSplash! Silence again."
26 |

Calling print(haiku) results in:

27 |
An old silent pond
28 | A frog jumps into the pond—
29 | Splash! Silence again.
30 |

The other most useful escape sequence is "\t" for inserting a tab character into a string.

31 |

String conversion

32 |

The code below attempts to concatenate a string with a number:

33 |
count = 3
34 | noun = 'cat'
35 | plural = 'I have ' + count + ' ' + noun + 's'
36 |

Sadly, that's en error! Python lets you use the + operator between two numbers and between two strings, but not between a number and a string.

37 |

One approach is to convert the number to a string, by calling the globally available str() on it.

38 |
plural = 'I have ' + str(count) + ' ' + noun + 's'
39 |

You can use str() in any situation where a function or operator expects a string but all you have is a number or some other data type.

40 |

F strings

41 |

Since programmers very often need to create new strings from a mish-mash of strings, numbers, and other variables, Python offers multiple ways to do string formatting. The most modern technique, and my own personal favorite, is f strings.

42 |

An f string starts with an "f" in front of the quotes, and then uses curly brackets to surround Python expressions inside the string. See this example:

43 |
greeting = "Ahoy"
44 | noun = "Boat"
45 | 
46 | sentence = f"{greeting}, {noun}yMc{noun}Face"
47 |

Python evaluates the expressions inside each curly bracket, turning that whole string into "Ahoy, BoatyMcBoatFace".

48 |

If the expression results in a numeric value, Python will take care of the string conversion for you:

49 |
month = "June"
50 | day = "24"
51 | year = "1984"
52 | 
53 | sentence = f"I was born {month} {day}th {year}"
54 |

The coolest thing about f strings is that you can put any valid expression inside those curly brackets - not just variable names.

55 |
quantity = 3
56 | price = 9.99
57 | 
58 | cashier_says = f"The total for your {quantity} meals will be {quantity * price}"
59 | 60 |
61 | ➡️ Next up: Exercise: String formatting 62 |
63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/2-exercise-string-formatting.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}String formatting{% endblock %} 3 | {% block content %} 4 |

Exercise: String formatting

5 |

Use f strings to complete these functions that return strings based on the input parameters.

6 | 7 |

Solar system

8 | def solar_system(num_planets): 9 | """Returns a string like 10 | "There are NUM planets in the solar system" 11 | where NUM is provided as an argument. 12 | 13 | >>> solar_system(8) 14 | 'There are 8 planets in the solar system' 15 | >>> solar_system(9) 16 | 'There are 9 planets in the solar system' 17 | """ 18 | return "" # REPLACE THIS LINE 19 | 20 |

Location

21 | def location(place, lat, lng): 22 | """Returns a string which starts with the provided PLACE, 23 | then has an @ sign, then the comma-separated LAT and LNG 24 | 25 | >>> location("Tilden Farm", 37.91, -122.29) 26 | 'Tilden Farm @ 37.91, -122.29' 27 | >>> location("Salton Sea", 33.309,-115.979) 28 | 'Salton Sea @ 33.309, -115.979' 29 | """ 30 | return "" # REPLACE THIS LINE 31 | 32 |

Fancy date

33 | def fancy_date(month, day, year): 34 | """Returns a string of the format 35 | "On the DAYth day of MONTH in the year YEAR" 36 | where DAY, MONTH, and YEAR are provided. 37 | For simplicity, "th" is always the suffix, not "rd" or "nd". 38 | 39 | >>> fancy_date("July", 8, 2019) 40 | 'On the 8th day of July in the year 2019' 41 | >>> fancy_date("June", 24, 1984) 42 | 'On the 24th day of June in the year 1984' 43 | """ 44 | return "" # REPLACE THIS LINE 45 | 46 |

Dr. Evil

47 | def dr_evil(amount): 48 | """Returns '<amount> dollars', adding '(pinky)' at the end 49 | if the amount is 1 million or more. 50 | 51 | >>> dr_evil(10) 52 | '10 dollars' 53 | >>> dr_evil(1000000) 54 | '1000000 dollars (pinky)' 55 | >>> dr_evil(2000000) 56 | '2000000 dollars (pinky)' 57 | """ 58 | # YOUR CODE HERE 59 | 60 |
61 | ➡️ Next up: String operations 62 |
63 | {% endblock %} 64 | {% block footer_scripts %} 65 | {% include "_code-exercise-script.html" %} 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/4-exercise-string-operations.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: String operations{% endblock %} 3 | {% block content %} 4 |

Exercise: String operations

5 |

Try out different string operations and methods in the following coding exercises. You probably want to keep the previous articles open in other tabs, as well as the Python string methods documentation.

6 | 7 |

String methods

8 | 9 |

The next two exercises help you practice using string methods.

10 | 11 |

Bleeper

12 |

Implement bleep_out, a function that looks for a particular phrase in a string and returns a new string with that phrase replaced by '***'.

13 | 14 |
15 | If you'd like more guidance, expand this block. 16 | 20 |
21 | def bleep_out(string1, string2): 22 | """Returns a version of string1 where all instances of string2 23 | have been replaced by "***". 24 | 25 | >>> bleep_out('i freaking love carrots', 'freaking') 26 | 'i *** love carrots' 27 | >>> bleep_out('oh dang oh dang oh dang', 'dang') 28 | 'oh *** oh *** oh ***' 29 | """ 30 | return "" # REPLACE THIS LINE 31 | 32 | 33 | 34 |

Phone number formats

35 |

Implement phone_number_versions, a function that turns a hyphen-separated phone number into a list containing that original number, a space-separated version of it, and a period-separated version.

36 | 37 |
38 | If you'd like more guidance, expand this block. 39 | 43 |
44 | def phone_number_versions(phone_number): 45 | """ 46 | Accepts a hyphen-separated phone number and returns 47 | a list containing the original version, a space-separated version, 48 | and a period-separated version. 49 | 50 | >>> phone_number_versions('1-800-867-5309') 51 | ['1-800-867-5309', '1 800 867 5309', '1.800.867.5309'] 52 | """ 53 | return [] # REPLACE THIS LINE 54 | 55 | 56 | 57 |

String slicing

58 | 59 |

The next three exercises help you practice using string slicing.

60 | 61 |

Mix up

62 |

Implement mix_up, a function that concatenates two words, but swaps out the first 2 characters of each word. For example, 'mix' and 'pod' is turned into 'pox mid'.

63 | def mix_up(word1, word2): 64 | """Returns the concatenation of word1 and word2 separated by a space, 65 | with the first 2 characters of each word swapped. 66 | Assumes the two words are at least 2 characters long. 67 | 68 | >>> mix_up('mix', 'pod') 69 | 'pox mid' 70 | >>> mix_up('dog', 'dinner') 71 | 'dig donner' 72 | """ 73 | return "" # REPLACE THIS LINE 74 | 75 | 76 |

Gerundio

77 |

Implement gerundio, a function that transforms Spanish infinite verbs (such as "cantar" or "volver") into their gerund forms ("cantando" and "volviendo"). See the docstring for the transformation rules.

78 |

Once you get the initial tests passing, you can also implement this function for another language you know that has similar infinitive → gerund verb form changes. Share it with the rest of us if you do!

79 | def gerundio(verb): 80 | """Turns Spanish infinitive verbs (which end in "ar", "er", "ir") 81 | into the gerund forms (which end in "ando" or "iendo). 82 | When a verb ends in "ar", the "ar" is replaced with "ando". 83 | When a verb ends in "er" or "ir", the end is replaced with "iendo". 84 | 85 | >>> gerundio('cantar') 86 | 'cantando' 87 | >>> gerundio('volver') 88 | 'volviendo' 89 | >>> gerundio('abrir') 90 | 'abriendo' 91 | >>> gerundio('armar') 92 | 'armando' 93 | """ 94 | return '' # REPLACE THIS LINE 95 | 96 |

Not bad? Good!

97 |

Implement not_bad, a function that replaces a phrase in a string that starts with "not" and ends with "bad" into the single word "good".

98 | def not_bad(sentence): 99 | """Returns a new string where the first substring in a sentence 100 | that starts with "not" and ends with "bad" is replaced by the word "good". 101 | Returns the original string if no matching substring is found. 102 | 103 | >>> not_bad('This dinner is not that bad!') 104 | 'This dinner is good!' 105 | >>> not_bad('This movie is not so bad!') 106 | 'This movie is good!' 107 | >>> not_bad('This dinner is bad!') 108 | 'This dinner is bad!' 109 | >>> not_bad('This movie is not so bad, dont you think?') 110 | 'This movie is good, dont you think?' 111 | """ 112 | return '' # REPLACE THIS LINE 113 | 114 | 115 |
116 | ➡️ Next up: Dictionaries 117 |
118 | {% endblock %} 119 | {% block footer_scripts %} 120 | {% include "_code-exercise-script.html" %} 121 | {% endblock %} 122 | -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/7-randomness.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Randomness{% endblock %} 3 | {% block content %} 4 |

Randomness

5 |

Most of the times, we want our programs to be very predictable; to always give the same output when given the same inputs.

6 |

However, if we're writing programs to generate art, graphics, or entertainment, we might actually want variability from our programs, to delight users with different output each time. Randomness to the rescue!

7 |

Try out the program below to see what I mean:

8 |
import random
  9 | 
 10 | rand_num = random.randint(1, 10000)
 11 | rand_char = chr(rand_num)
 12 | 
 13 | print(rand_char)
14 |

That program first imports the Python random module, which contains a bunch of functions for generating randomness. It then uses random.randint(start, end) to generate an integer between the start and end integers (inclusive of both the start and end). Finally, it calls the global function chr(int) to turn that number into a Unicode character. If you're curious, you can also print out the generated rand_num to see what the number was before it got turned into a character.

15 |

Using randomness inside functions

16 |

We sometimes write functions that can generate a random result, like a random color, a random x,y point, or even an entire shuffled deck of cards.

17 |

Try running the function below a few times:

18 |
def random_rgb():
 19 |     r = random.randint(0, 255)
 20 |     g = random.randint(0, 255)
 21 |     b = random.randint(0, 255)
 22 |     return [r, g, b]
 23 | 
 24 | random_rgb()
25 |

I used the function above to make a randomized animated pixels demo. The code for that demo also relies on Pygame, a popular library for making games in Python.

26 |

A screenshot is below, but check out the link above to see the code and demo. The demo is hosted on Repl.it since Pygame doesn't work in the embedded code editor used in the articles.

27 |

28 |

Testing randomness

29 |

Before randomness, all of our functions were deterministic: for a given input, there's a predictable output.

30 |

Now, a given input (or no input at all) can result in very different results each time. That makes testing harder, since we can't test for exact outputs; we can only make sure the output is within some reasonable range.

31 |

Here's how we might write tests for that random color function:

32 |
def random_rgb():
 33 |     """Returns a list of random R, G, B color values.
 34 |     
 35 |     >>> color = random_rgb()
 36 |     >>> len(color) == 3
 37 |     True
 38 |     >>> color[0] >= 0 and color[0] <= 255
 39 |     True
 40 |     >>> color[1] >= 0 and color[1] <= 255
 41 |     True
 42 |     >>> color[2] >= 0 and color[2] <= 255
 43 |     True
 44 |     """
 45 |     r = random.randint(0, 255)
 46 |     g = random.randint(0, 255)
 47 |     b = random.randint(0, 255)
 48 |     return [r, g, b]
49 |

In Python at least, there is one other way of testing a function that relies on randomness. You can initialize the random number generator using a particular seed, and that seed will always result in the same sequence of random values. Typically, the seed is initialized based on the current system time, making output appear different each time, but we can set it to a constant number in our tests.

50 |

Once setting the seed, we can write a standard input/output test:

51 |
def random_rgb():
 52 |     """Returns a list of random R, G, B color values.
 53 |     
 54 |     >>> random.seed(1)
 55 |     >>> random_rgb()
 56 |     [106, 184, 0]
 57 |     """
 58 |     r = random.randint(0, 255)
 59 |     g = random.randint(0, 255)
 60 |     b = random.randint(0, 255)
 61 |     return [r, g, b]
62 |

Just make sure you don't set the seed in the function code itself, or you'll end up with very not-random output.

63 |

Random list items

64 |

Consider this list of random congratulations messages:

65 |
yays = ["You got it!", "Congrats!", "Well done!", "Nice one!"]
66 |

The co:rise engineers might want to generate a random message each time a student gets a quiz answer correct.

67 |

One way to do that is to use the randint function from before, and use that to generate an index:

68 |
yays[random.randint(0, len(yays)-1)]
69 |

And that would totally work! But this is such a common need that Python added a function just for this, random.choice(sequence).

70 |

Thanks to that function, the code can become much cleaner and concise:

71 |
random.choice(yays)
72 |

Pseudo-randomness

73 |

Alright, now it's time for some real talk: none of this randomness is truly random. Sorry! The problem is, computers can't truly be random: they're built from logical circuits computing 0s and 1s, they don't know how to act on a whim.

74 |

Computers instead use pseudo-random number generators (PRNGs) which generate seemingly random numbers using a sequence of mathematical computations.

75 |

If for some reason a computer program needed truly random data, it would need to find a random process in nature and generate numbers based on that. The website random.org claims that it actually does generate random numbers by sampling atmospheric noise, and they go into great detail about their generation and attempts to prove the true randomness of the results. Fascinating!

76 |

Fortunately, the results of PRNGs are typically random enough to satisfy most use cases. Try out the program below which generates the numbers 1-3 a thousand times and measures the frequency of the results. Does it seem random enough for you?

77 | 78 | import random 79 | 80 | num_experiments = 10000 81 | 82 | one_called = 0 83 | two_called = 0 84 | three_called = 0 85 | 86 | for _ in range(num_experiments): 87 | random_num = random.randint(1, 3) 88 | if random_num == 1: 89 | one_called += 1 90 | elif random_num == 2: 91 | two_called += 1 92 | elif random_num == 3: 93 | three_called += 1 94 | 95 | def report_frequency(num, call_count, total_count): 96 | percentage = call_count/total_count 97 | print(f"{num} called {call_count} times: {percentage}") 98 | 99 | report_frequency(1, one_called, num_experiments) 100 | report_frequency(2, two_called, num_experiments) 101 | report_frequency(3, three_called, num_experiments) 102 | 103 |
104 | ➡️ Next up: Files 105 |
106 | {% endblock %} 107 | 108 | 109 | {% block footer_scripts %} 110 | {% include "_code-exercise-script.html" %} 111 | {% endblock %} -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/8-files.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Files{% endblock %} 3 | {% block content %} 4 |

Files

5 |

One source of input for computer programs is a file stored in the local file system. You could write a program that checked a text document for spelling errors, or like you did in project 2, a program to apply filters to an image file.

6 |

There are two types of files:

7 |

Text files contain one or more lines that contain text characters, encoded according to a character encoding (like UTF-8). Each line in a text file ends with a control character. Unfortunately, the character varies based on the operating system. When a text file is created on a Mac, lines end with "\n". For Unix-created files, lines end with "\r". And annoyingly, on Windows, lines end with both characters, "\r\n". We'll need to keep this in mind when we read a file, if we're trying to break it up into lines.

8 |

Anything that's not a text file is a binary file. The bytes in a binary file are intended to be interpreted in some other way, typically dictated by the file extension. For example, all of these are binary files:

9 | 13 |

Handling files

14 |

This is the general 3-step process for handling files in Python:

15 |
  1. Open the file

  2. 16 |
  3. Either read data from file or write data to file

  4. 17 |
  5. Close the file

18 |

It's necessary to explicitly close the file at the end, since if you don't, the operating system won't let any other processes mess with that file.

19 |

Let's see how to write the code to make those steps happen.

20 |

Opening files

21 |

The global built-in function open(file, mode) opens a file and returns a file object. The first argument is the path to the file, while the second argument specifies what we want to do with that file, like read or write it. The following code opens "myinfo.txt" in a read-only mode:

22 |
open("myinfo.txt", mode="r")
23 |

Here are the most common modes:

24 |

Mode Meaning Description r Read If the file does not exist, this raises an error. w Write If the file does not exist, this mode creates it. If file already exists, this mode deletes all data. a Append If the file does not exist, this mode creates it. If file already exists, this mode appends data to end of existing data. b Binary Use for binary files along with r or w more.

25 |

The function also takes additional optional arguments and modes, described in the docs.

26 |

Reading the file

27 |

Once we have a file object, there are several methods that we can use to read the contents of the file.

28 |

The read method reads the entire contents of the file into a string:

29 |
my_file = open("myinfo.txt", mode="r")
30 | my_info = my_file.read()
31 |

The readlines method reads the contents into a list of strings, where each string is a line of the file. That's handy since it takes care of the cross-OS issues with different line end characters ("\r" vs. "\n").

32 |
my_file = open("myinfo.txt", mode="r")
33 | file_lines = my_file.readlines()
34 | 
35 | for line in file_lines:
36 |     print(line)
37 |

Python also provides an option for reading a file lazily - just one line at a time - by using a for loop.

38 |
rows = []
39 | my_file =  open("longbook.txt", mode="r")
40 | for line in my_file:
41 |     rows.append(line)
42 |

If we allow that loop to iterate all the way to the end of the file, then there's no difference between that and readlines. However, we could break out of the loop once we've found something in the file, like so:

43 |
rows = []
44 | my_file =  open("longbook.txt", mode="r")
45 | for line in my_file:
46 |     rows.append(line)
47 |      if line.find('Chapter 2') > -1:
48 |             break
49 |

This is a great approach for very long text files, since it means you don't have to read the whole darn file into memory.

50 |

Writing files

51 |

To write a file, we need to first open it in either the "w" mode, which will empty out the file upon opening it, or the "a" mode, which will keep the prior contents but append additional data to the end.

52 |

Overwriting the entire file:

53 |
my_file = open("myinfo.txt", mode="w")
54 | my_file.write("Birth city: Pasadena, CA")
55 |

Appending to the existing file contents:

56 |
my_file = open("myinfo.txt", mode="a")
57 | my_file.write("First pet: Rosarita (Rabbit)")
58 |

Closing files

59 |

Finally, once we're done reading or writing, we need to close the file. The close() method closes the file, ending all operations and freeing up resources.

60 |
my_file = open("myinfo.txt", mode="r")
61 | my_file.close()
62 |

A fairly different approach is to use a with statement to open the file, and then put all the reading and writing calls inside the body of the statement.

63 |
with open("myinfo.txt", mode="r") as my_file:
64 |     lines = my_file.readlines()
65 |     my_file.close()
66 | 
67 | print(lines)
68 |

Once all the statements indented inside the with block are executed, Python takes care of closing the file for you. Any code that runs after the with block would not be able to read or write that file, since it's no longer open.

69 |

Some programmers prefer the second approach since you only need to remember to open the file. But either approach is fine, whatever floats your boat! 🛶

70 |

Online files

71 |

The open(path) function only works for opening local files in the file system. What if there's a file online that you want to work with? I actually work more with online files than local files, myself!

72 |

In the Python standard library, the urllib.request module includes a urlopen(url) function in the urllib module that can open a file at a URL and return a file-like object.

73 |

This code opens a text file that contains an entire book, The Count of Monte Cristo:

74 |
import urllib.request
75 | 
76 | text_file = urllib.request.urlopen('https://www.gutenberg.org/cache/epub/1184/pg1184.txt')
77 |

Once the file is opened, we can use similar methods as discussed above.

78 |

This line of code reads the whole book into a single string:

79 |
whole_book = text_file.read()
80 |

However, there's one significant difference between that string and the string returned when reading a local file. The variable above now stores a byte string, which Python displays with a lowercase b in front:

81 |
b'\xef\xbb\xbfThe Count of Monte Cristo, by Alexandre Dumas, p\xc3\xa8re.'
82 |

A byte string is a series of bytes (8-bit sequences), which is how computers actually store data behind the strings. Python byte strings do allow the first 128 bytes to be shown as letters, but there are thousands of characters beyond those. That's why character encodings exist, to specify how a sequence of bytes corresponds to a particular character. The most common encoding is UTF-8, especially for files in English or European languages.

83 |

In order to translate that byte string into a string of characters, we must know the encoding of the original data, and then call decode(encoding) on the byte string.

84 |

Since that book file was indeed encoded with UTF-8, we can decode it like this:

85 |
whole_book = whole_book.decode('utf-8')
86 |

Now, instead of seeing b'p\xc3\xa8re', we'll see 'père' in the string.

87 |

Once decoded, we can use string operations on it, such as using split to turn it into a list of lines.

88 | 89 |
90 | ➡️ Next up: Project 3: Text generator 91 |
92 | {% endblock %} -------------------------------------------------------------------------------- /content/3-strings-&-dictionaries/9-project-3-text-generator.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Project 3: Text generator{% endblock %} 3 | {% block content %} 4 |

Project 3: Text generator

5 |

In this project, you'll be building a procedural text generator. Procedural generation is any way of using algorithms to create data (versus manually creating data), and is commonly used in games and movies - to create organic looking landscapes, intricate mazes, beautiful snowflakes, crowds of people, and fictitious text.

6 |

You'll be using a technique called a Markov chain that generates text based on the probability of one word coming after another word, which it learns from whatever original data you feed it. You could generate text based on tweets, wise sayings, movie titles, sentences from your favorite author, ...whatever strikes your fancy!

7 |

Instructions

8 |

You'll be using a Python notebook on Google CoLab to develop this project.

9 |

To get started, make a copy of the Project 3 notebook for yourself. The rest of the instructions are in the notebook.

10 | 11 |
12 | ➡️ Next unit: Object-Oriented Programming 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/0-unit-overview.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Unit 4: Object-Oriented Programming{% endblock %} 3 | {% block content %} 4 |

Unit 4: Object-Oriented Programming

5 |

It's the fourth and final unit! This unit is focused on object-oriented programming, a powerful way to organize the data and functions in programs.

6 |

Our learning goals for this unit are:

7 | 11 |

At the end, you'll combine those skills to make a quiz on the topic of your choosing.

12 | 13 |
14 | ➡️ Next up: Object-oriented programming 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /content/4-object-oriented-programming/1-object-oriented-programming.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Object-oriented programming{% endblock %} 3 | {% block content %} 4 |

Object-oriented programming

5 |

Our programs so far have consisted of functions intermixed with variables. This is a common way of writing programs when they are small and focused on computational functions, but as programs become larger and revolve more around data, Python programmers often use an approach known as object-oriented programming (OOP).

6 |

Object-oriented programming includes:

7 | 9 |

We can have multiple instances of each type of data, and we consider each of them to be an object. For example, my savings account could be one object and my checking account could be another object. Since they're both bank accounts, I could deposit and transfer into either of them.

10 |

A diagram showing a deposit into a checking account and a transfer into a savings account

11 |

There might be some functionality specific to savings accounts versus checking accounts, and object-oriented programming also gives us a way to declare that a particular type of object is similar to another type, but also differs in some ways.

12 |

A tree diagram showing a Bank Account type with Checking Account and Saving Account types below

13 |

As it turns out, we've been using objects this whole time: every piece of data in Python is an object of a particular type. Integers are type int, floating point numbers are type float, strings are type string, lists are type list, dictionaries are type dict. Each of the types have associated functionality (methods), and that's why we can call split() on any string object, or call pop() on any list object.

14 |

What we'll learn this unit is how we can declare our own types of objects, thanks to a mechanism called classes. Let's dive in!

15 |
16 | ➡️ Next up: Classes 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/10-composition.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Composition{% endblock %} 3 | {% block content %} 4 |

Composition

5 |

Another key part of object-oriented programming is composition: an object can be composed of other objects. In Python, since every piece of data (number, boolean, string, etc.) is an object, technically every example we've seen so far has been an example of composition. However, I want to dive into examples where an object of a user-defined class contains references to other objects of user-defined classes, since that's more interesting.

6 |

Going back to our animal conservatory, what examples of composition could there be? A few ideas:

7 | 10 |

We can implement all of those examples using instance variables that either store a single object or a list of objects.

11 |

Composition with a single object

12 |

Here, let's code a method for mating one animal with another animal:

13 |
class Animal:
14 | 
15 |     def mate_with(self, other):
16 |         if other is not self and other.species_name == self.species_name:
17 |             self.mate = other
18 |             other.mate = self
19 |

That method first checks to make sure we're not trying to mate an animal with itself, and also ensures the animals are the same species, since we want the best for their reproductive future. If that all checks out, then it sets a new instance variable mate to the other animal object passed in, and also sets the mate instance variable of the other animal to itself. That sets up a symmetric relationship where both objects are pointing at each other.

20 |

Here's how we call that method:

21 |
mr_wabbit = Rabbit("Mister Wabbit", 3)
22 | jane_doe = Rabbit("Jane Doe", 2)
23 | mr_wabbit.mate_with(jane_doe)
24 |

Composition with a list

25 |

An instance variable could also store a list of objects.

26 |

Read through this code that simulates rabbit reproduction:

27 |
class Rabbit(Animal):
28 | 
29 |     # Other methods/class variables omitted for brevity
30 | 
31 |     def reproduce_like_rabbits(self):
32 |         if self.mate is None:
33 |             print("oh no! better go on ZoOkCupid")
34 |             return
35 |         self.babies = []
36 |         for _ in range(self.num_in_litter):
37 |             self.babies.append(Rabbit("bunny", 0))
38 |

It first makes sure the rabbit has a mate, since rabbits don't reproduce asexually. It then initializes the babies instance variable to an empty list, and uses loop to add a bunch of new rabbits to that list with an age of 0.

39 |

We can call that method after setting up the mate relationship:

40 |
mr_wabbit = Rabbit("Mister Wabbit", 3)
41 | jane_doe = Rabbit("Jane Doe", 2)
42 | mr_wabbit.mate_with(jane_doe)
43 | jane_doe.reproduce_like_rabbits()
44 |

This composition isn't perfect: a better approach might be to initialize babies inside __init__, so that a rabbit could reproduce multiple times and keep growing its list of babies each time. It also might be a good thing to add a line like self.mate.babies = self.babies or self.mate.babies.extend(self.babies) so that the mate could also keep track of the produced babies. The best implementation really depends on the use case for the compositional relationship and how the rest of the program will use that information.

45 |

Inheritance vs. composition

46 |

You've now learned two ways that classes and objects can relate to each other, composition and inheritance. Let's compare them.

47 |

Inheritance is best for representing "is-a" relationships.

48 | 50 |

Composition is best for representing "has-a" relationships.

51 | 53 |

When you're designing a system of classes, keep in mind how objects relate to each other so you can figure out the most appropriate way to represent that in code. Sometimes it helps to draw before you code, sketching the relationships out on paper or making a formal diagram using UML (Unified Modeling Language).

54 | 55 |
56 | ➡️ Next up: Polymorphism 57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/11-polymorphism.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Polymorphism{% endblock %} 3 | {% block content %} 4 |

Polymorphism

5 |

One of the great benefits of object-oriented programming is polymorphism: different types of objects sharing the same interface.

6 |

Polymorphism from inheritance

7 |

We've already seen many polymorphic objects, since inheritance almost always leads to polymorphism.

8 |

For example, we can call the eat method on both Panda object and Lion objects, passing in the same argument:

9 |
broc = Meal("Broccoli", "veggie")
10 | pandey = Panda("Pandeybear", 6)
11 | mufasa = Lion("Mufasa", 12)
12 | 
13 | pandey.eat(broc)
14 | mufasa.eat(broc)
15 |

Now, the two objects may react differently to that method, due to differing implementations. The panda might say "Mmm yummy" and the lion might say "Gross, veggies!". But the point of polymorphism is that the interface was the same - the method name and argument types - which meant we could call that interface without knowing the exact details of the underlying implementation.

16 |

When we have a list of polymorphic objects, we can call the same method on each object even if we don't know exactly what type of object they are.

17 |

Consider this function that throws an animal party, making each animal in a list interact_with the other animals in the list:

18 |
def partytime(animals):
19 |     for i in range(len(animals)):
20 |         for j in range(i + 1, len(animals)):
21 |             animals[i].interact_with(animals[j])
22 |

That list of animals can contain any Animal instance, since they all implement interact_with in some way.

23 |
jane_doe = Rabbit("Jane Doe", 2)
24 | scar = Lion("Scar", 12)
25 | elly = Elephant("Elly", 5)
26 | pandy = Panda("PandeyBear", 4)
27 | partytime([jane_doe, scar, elly, pandy])
28 |

That right there is the power of polymorphism.

29 |

Polymorphism from method interfaces

30 |

We've also seen a lot of polymorphism in the built-in Python object types, even though we haven't called it that.

31 |

For example, consider how many types we can pass to the len function:

32 |
len([1, 2, 3])
33 | len("ahoy there!")
34 | len({"CA": "Sacramento", "NY": "Albany"})
35 | len(range(5))
36 |

The len() function is able to report the length of lists, strings, dicts, and ranges. All of those inherit from object, but len() doesn't work on any old object. So why does it work for those types?

37 |

It's actually because the list, str, dict, and range classes each define a method named __len__ that reports their length using various mechanisms. The global len() function works on any object that defines a __len__ method and returns a number. That means we can even make len() work on our own user-defined classes.

38 |

This type of polymorphism in a language is also known as duck typing: if it walks like a duck and quacks like a duck, then it must be a duck. 🦆The len() function doesn't care exactly what kind of object it's passed: if it has a __len__ method and that method takes 0 arguments, then that's good enough for Python.

39 |

Many languages are more strict than Python, and possibly for good reason, since you can get yourself in trouble with loosey-goosey duck typing. But you can also write highly flexible code. Tread carefully in these polymorphic waters!

40 | 41 |
42 | ➡️ Next up: Project 4: OOP quiz 43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /content/4-object-oriented-programming/12-project-4-oop-quiz.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Project 4: OOP Quiz{% endblock %} 3 | {% block content %} 4 |

Project 4: OOP Quiz

5 |

For this project, you're going to use object-oriented programming to write a quiz that can handle different kinds of questions - and you'll use it to deliver a quiz on a topic of your choosing.

6 |

The code for this project is inspired by many codebases that I've worked on myself: Khan Academy's exercises, Coursera's timed quizzes, Berkeley's online CS exams. All of them have used a slightly different class design, but they've all used the power of OOP to describe the structure of quizzes and the questions inside them.

7 |

I look forward to trying out all of your quizzes!

8 |

Instructions

9 |

You'll be using a Python notebook on Google CoLab to develop this project.

10 |

To get started, make a copy of the Project 4 notebook for yourself. The rest of the instructions are in the notebook.

11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/3-exercise-classes.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: Classes{% endblock %} 3 | {% block content %} 4 |

Exercise: Classes

5 |

In the following exercises, you'll implement the __init__ and other methods of a class. Make sure you first read through the class doctests to see what instance variables and methods are expected to exist on each class.

6 | 7 |

Plant

8 |

Finish the implementation of Plant, a class that can be used by a plant nursery to track which plants they have in inventory and how many they have of each plant.

9 |

📺 Need help with this one? Watch a video walkthrough of the solution. Then try it out yourself and apply what you learned to the exercises after.

10 |

🧩 Or, for a different form of guidance, try a Parsons Puzzle version first.

11 | class Plant: 12 | """A class used by a plant nursery to track what plants they have in inventory 13 | and how many they have of each plant. 14 | 15 | >>> milkweed = Plant("Narrow-leaf milkweed", "Asclepias fascicularis") 16 | >>> milkweed.common_name 17 | 'Narrow-leaf milkweed' 18 | >>> milkweed.latin_name 19 | 'Asclepias fascicularis' 20 | >>> milkweed.inventory 21 | 0 22 | >>> milkweed.update_inventory(2) 23 | >>> milkweed.inventory 24 | 2 25 | >>> milkweed.update_inventory(3) 26 | >>> milkweed.inventory 27 | 5 28 | """ 29 | 30 | def __init__(self, common_name, latin_name): 31 | self.common_name = common_name 32 | # Fill out the rest of this function 33 | 34 | def update_inventory(self, amount): 35 | # Fill out this function 36 | 37 | 38 |

MoviePurchase

39 |

Finish the implementation of MoviePurchase, a class that represents movie purchases on YouTube, tracking the title and costs of each movie bought, plus the number of times watched.

40 |

🧩 For more guidance, try a Parsons Puzzle version first.

41 | 42 | class MoviePurchase: 43 | """A class that represents movie purchases on YouTube, 44 | tracking the title and cost of each movie bought, 45 | as well as the number of times the movie is watched. 46 | 47 | >>> ponyo = MoviePurchase("Ponyo", 19.99) 48 | >>> ponyo.title 49 | 'Ponyo' 50 | >>> ponyo.cost 51 | 19.99 52 | >>> ponyo.times_watched 53 | 0 54 | >>> ponyo.watch() 55 | >>> ponyo.watch() 56 | >>> ponyo.times_watched 57 | 2 58 | """ 59 | 60 | def __init__(self, title, cost): 61 | # Fill out this function 62 | 63 | def watch(self): 64 | # Fill out this function 65 | 66 |

Player

67 |

Finish the implementation of Player, a class that represents a player in a video game, tracking their name and health.

68 |

🧩 For more guidance, try a Parsons Puzzle version first.

69 | 70 | class Player: 71 | """A class that represents a player in a video game, 72 | tracking their name and health. 73 | 74 | >>> player = Player("Mario") 75 | >>> player.name 76 | 'Mario' 77 | >>> player.health 78 | 100 79 | >>> player.damage(10) 80 | >>> player.health 81 | 90 82 | >>> player.boost(5) 83 | >>> player.health 84 | 95 85 | """ 86 | 87 | def __init__(self, name): 88 | # YOUR CODE HERE 89 | 90 | def damage(self, amount): 91 | # YOUR CODE HERE 92 | 93 | def boost(self, amount): 94 | # YOUR CODE HERE 95 | 96 |

Clothing

97 |

Implement Clothing, a class that represents pieces of clothing in a closet, tracking their color, category, and clean/dirty state.

98 |

🧩 For more guidance, try a Parsons Puzzle version first.

99 | 100 | class Clothing: 101 | """A class that represents pieces of clothing in a closet, 102 | tracking the color, category, and clean/dirty state. 103 | 104 | >>> blue_shirt = Clothing("shirt", "blue") 105 | >>> blue_shirt.category 106 | 'shirt' 107 | >>> blue_shirt.color 108 | 'blue' 109 | >>> blue_shirt.is_clean 110 | True 111 | >>> blue_shirt.wear() 112 | >>> blue_shirt.is_clean 113 | False 114 | >>> blue_shirt.clean() 115 | >>> blue_shirt.is_clean 116 | True 117 | """ 118 | 119 | # Write an __init__ method 120 | 121 | # Write the wear() method 122 | 123 | # Write the clean() method 124 | 125 | 126 |
127 | ➡️ Next up: More on classes 128 |
129 | {% endblock %} 130 | 131 | {% block footer_scripts %} 132 | {% include "_code-exercise-script.html" %} 133 | {% endblock %} 134 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/5-exercise-more-on-classes.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: More on classes{% endblock %} 3 | {% block content %} 4 |

Exercise: More on classes

5 |

These exercises will give you practice with class variables and class methods.

6 |

Missing class variables

7 |

In the first two exercises, the class definitions are missing a class variable. Read the docstring and doctests to determine the name and value of the missing class variable, add it in, and run the doctests after.

8 |

Exercise: StudentGrade

9 |

📺 Need help with this one? Watch a video walkthrough of the solution. Then try it out yourself and apply what you learned to the next exercise.

10 | class StudentGrade: 11 | """Represents the grade for a student in a class 12 | and indicates when a grade is failing. 13 | For all students, 159 points is considered a failing grade. 14 | 15 | >>> grade1 = StudentGrade("Arfur Artery", 300) 16 | >>> grade1.is_failing() 17 | False 18 | >>> grade2 = StudentGrade("MoMo OhNo", 158) 19 | >>> grade2.is_failing() 20 | True 21 | >>> grade1.failing_grade 22 | 159 23 | >>> grade2.failing_grade 24 | 159 25 | >>> StudentGrade.failing_grade 26 | 159 27 | >>> 28 | """ 29 | 30 | def __init__(self, student_name, num_points): 31 | self.student_name = student_name 32 | self.num_points = num_points 33 | 34 | def is_failing(self): 35 | return self.num_points < StudentGrade.failing_grade 36 | 37 |

Exercise: Article

38 | class Article: 39 | """Represents an article on an educational website. 40 | The license for all articles should be "CC-BY-NC-SA". 41 | 42 | >>> article1 = Article("Logic", "Samuel Tarín") 43 | >>> article1.get_byline() 44 | 'By Samuel Tarín, License: CC-BY-NC-SA' 45 | >>> article2 = Article("Loops", "Pamela Fox") 46 | >>> article2.get_byline() 47 | 'By Pamela Fox, License: CC-BY-NC-SA' 48 | """ 49 | 50 | def __init__(self, title, author): 51 | self.title = title 52 | self.author = author 53 | 54 | def get_byline(self): 55 | return f"By {self.author}, License: {self.license}" 56 | 57 |

Class methods

58 |

Exercise: Thneed Factory

59 |

For this exercise, you'll finish the implementation of make_thneed, a class method that can make Thneeds of a given size and a random color.

60 |

Hint: You can use random.choice(list) to generate a random color from a list of colors.

61 |

📺 Need help with this one? Watch the video walkthrough of the solution. Then try it out yourself and apply what you learned to the next exercise.

62 | import random 63 | 64 | class Thneed: 65 | def __init__(self, color, size): 66 | self.color = color 67 | self.size = size 68 | 69 | @classmethod 70 | def make_thneed(cls, size): 71 | """ 72 | Returns a new instance of a Thneed with 73 | a random color and the given size. 74 | >>> rand_thneed = Thneed.make_thneed("small") 75 | >>> isinstance(rand_thneed, Thneed) 76 | True 77 | >>> rand_thneed.size 78 | 'small' 79 | >>> isinstance(rand_thneed.color, str) 80 | True 81 | """ 82 | # YOUR CODE HERE 83 | 84 |

Exercise: Cat adoption

85 |

For this exercise, you'll finish the implementation of adopt_random_cat, a class method that can generate new cats. Just what we need in the world! 🐈‍⬛🐈

86 |

Hint: You can use random.choice(list) to generate a random name from a list of names and random.randrange(start, stop) to generate a random number of lives.

87 | import random 88 | 89 | class Cat: 90 | def __init__(self, name, owner, lives=9): 91 | self.name = name 92 | self.owner = owner 93 | self.lives = lives 94 | 95 | @classmethod 96 | def adopt_random_cat(cls, owner): 97 | """ 98 | Returns a new instance of a Cat with the given owner, 99 | a randomly chosen name and a random number of lives. 100 | >>> randcat = Cat.adopt_random_cat("Ifeoma") 101 | >>> isinstance(randcat, Cat) 102 | True 103 | >>> randcat.owner 104 | 'Ifeoma' 105 | """ 106 | # YOUR CODE HERE 107 | 108 |
109 | ➡️ Next up: Inheritance 110 |
111 | {% endblock %} 112 | {% block footer_scripts %} 113 | {% include "_code-exercise-script.html" %} 114 | {% endblock %} 115 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/6-inheritance.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Inheritance{% endblock %} 3 | {% block content %} 4 |

Inheritance

5 |

Languages that support object-oriented programming typically include the idea of inheritance to allow for greater code re-use and extensibility, and Python is one of those languages.

6 |

To see what we mean by inheritance, let's imagine that we're building a game, "Animal Conserving", that simulates an animal conservatory. We'll be taking care of both fuzzy and ferocious animals in this game, making sure they're well fed and cared for.

7 |

Pairs of animals playing and eating

8 |

We're going to use OOP to organize the data in the game. What do you think the classes should be?

9 |

Here's one approach:

10 |
# A class for meals
 11 | Meal()
 12 | 
 13 | # A class for each animal
 14 | Panda()
 15 | Lion()
 16 | Rabbit()
 17 | Vulture()
 18 | Elephant()
19 |

Let's start writing out the class definitions, starting simple with the Meal class:

20 |
class Meal:
 21 | 
 22 |     def __init__(self, name, kind, calories):
 23 |         self.name = name
 24 |         self.kind = kind
 25 |         self.calories = calories
26 |

Here's how we would construct a couple meals:

27 |
broccoli = Meal("Broccoli Rabe", "veggies", 20)
 28 | bone_marrow = Meal("Bone Marrow", "meat", 100)
29 |

Now, some of the animal classes, starting with our long-trunked friend: 🐘

30 |
class Elephant:
 31 |     species_name = "African Savanna Elephant"
 32 |     scientific_name = "Loxodonta africana"
 33 |     calories_needed = 8000
 34 | 
 35 |     def __init__(self, name, age=0):
 36 |         self.name = name
 37 |         self.age = age
 38 |         self.calories_eaten  = 0
 39 |         self.happiness = 0
 40 | 
 41 |     def eat(self, meal):
 42 |         self.calories_eaten += meal.calories
 43 |         print(f"Om nom nom yummy {meal.name}")
 44 |         if self.calories_eaten > self.calories_needed:
 45 |             self.happiness -= 1
 46 |             print("Ugh so full")
 47 | 
 48 |     def play(self, num_hours):
 49 |         self.happiness += (num_hours * 4)
 50 |         print("WHEEE PLAY TIME!")
 51 | 
 52 |     def interact_with(self, animal2):
 53 |         self.happiness += 1
 54 |         print(f"Yay happy fun time with {animal2.name}")
55 |

Every elephant shares a few class variables, species_name, scientific_name, and calories_needed. They each have their own name, age, calories_eaten, and happiness instance variables, however.

56 |

Let's make a playful pair of elephants:

57 |
el1 = Elephant("Willaby", 5)
 58 | el2 = Elephant("Wallaby", 3)
 59 | el1.play(2)
 60 | el1.interact_with(el2)
61 |

Next, let's write a class for our cute fuzzy long-eared friends: 🐇

62 |
class Rabbit:
 63 |     species_name = "European rabbit"
 64 |     scientific_name = "Oryctolagus cuniculus"
 65 |     calories_needed = 200
 66 | 
 67 |     def __init__(self, name, age=0):
 68 |         self.name = name
 69 |         self.age = age
 70 |         self.calories_eaten = 0
 71 |         self.happiness = 0
 72 | 
 73 |     def play(self, num_hours):
 74 |         self.happiness += (num_hours * 10)
 75 |         print("WHEEE PLAY TIME!")
 76 | 
 77 |     def eat(self, food):
 78 |         self.calories_eaten += food.calories
 79 |         print(f"Om nom nom yummy {food.name}")
 80 |         if self.calories_eaten > self.calories_needed:
 81 |             self.happiness -= 1
 82 |             print("Ugh so full")
 83 | 
 84 |     def interact_with(self, animal2):
 85 |         self.happiness += 4
 86 |         print(f"Yay happy fun time with {animal2.name}")
87 |

And construct a few famous rabbits:

88 |
rabbit1 = Rabbit("Mister Wabbit", 3)
 89 | rabbit2 = Rabbit("Bugs Bunny", 2)
 90 | rabbit1.eat(broccoli)
 91 | rabbit2.interact_with(rabbit1)
92 |

Do you notice similarities between the two animal classes? The structure of the two classes have much in common:

93 | 96 |

So it appears that 90% of their code is in fact the same. That violates a popular programming principle, "DRY" (Don't Repeat Yourself), and personally, makes my nose crinkle a little in disgust. Repeated code is generally a bad thing because we need to remember to update that code in multiple places, and we're liable to forget to keep code in sync that's meant to be the same.

97 |

Fortunately, we can use inheritance to rewrite this code. Instead of repeating the code, Elephant and Rabbit can inherit the code from a base class.

98 |

Base class

99 |

When multiple classes share similar attributes, you can reduce redundant code by defining a base class and then subclasses can inherit from the base class.

100 |

For example, we can first write an Animal base class, put all the common code in there, and the specific animal species can be subclasses of that base class:

101 |

A tree diagram with Animal at the top and specific animal classes as nodes underneath it

102 |

You'll also hear the base class referred to as the superclass.

103 |

Here's how we could write the Animal base class:

104 |
class Animal:
105 |     species_name = "Animal"
106 |     scientific_name = "Animalia"
107 |     play_multiplier = 2
108 |     interact_increment = 1
109 | 
110 |     def __init__(self, name, age=0):
111 |         self.name = name
112 |         self.age = age
113 |         self.calories_eaten  = 0
114 |         self.happiness = 0
115 | 
116 |     def play(self, num_hours):
117 |         self.happiness += (num_hours * self.play_multiplier)
118 |         print("WHEEE PLAY TIME!")
119 | 
120 |     def eat(self, food):
121 |         self.calories_eaten += food.calories
122 |         print(f"Om nom nom yummy {food.name}")
123 |         if self.calories_eaten > self.calories_needed:
124 |             self.happiness -= 1
125 |             print("Ugh so full")
126 | 
127 |     def interact_with(self, animal2):
128 |         self.happiness += self.interact_increment
129 |         print(f"Yay happy fun time with {animal2.name}")
130 |

We even defined class variables in there. We didn't need to do that, since the values of those variables don't make sense, but it is helpful to show the recommended class variables for the subclasses.

131 |

Subclasses

132 |

To declare a subclass, put parentheses after the class name and specify the base class in the parentheses:

133 |
class Elephant(Animal):
134 |

Then the subclasses only need the code that's unique to them. They can redefine any aspect: class variables, method definitions, or constructor. A redefinition is called overriding.

135 |

Here's the full Elephant subclass, which only overrides the class variables:

136 |
class Elephant(Animal):
137 |     species_name = "African Savanna Elephant"
138 |     scientific_name = "Loxodonta africana"
139 |     calories_needed = 8000
140 |     play_multiplier = 4
141 |     interact_increment = 1
142 |     num_tusks = 2
143 |

Same for the Rabbit class:

144 |
class Rabbit(Animal):
145 |     species_name = "European rabbit"
146 |     scientific_name = "Oryctolagus cuniculus"
147 |     calories_needed = 200
148 |     play_multiplier = 10
149 |     interact_increment = 2
150 |     num_in_litter = 12
151 |

Overriding methods

152 |

A subclass can also override the methods of the base class. Python will always look for the method definition on the current object's class first, and will only look in the base class if it can't find it there.

153 |

We could override interact_with for pandas, since they're quite solitary creatures:

154 |
class Panda(Animal):
155 |     species_name = "Giant Panda"
156 |     scientific_name = "Ailuropoda melanoleuca"
157 |     calories_needed = 6000
158 | 
159 |     def interact_with(self, other):
160 |         print(f"I'm a Panda, I'm solitary, go away {other.name}!")
161 |

This code will call that overridden method definition instead of the Animal definition:

162 |
panda1 = Panda("Pandeybear", 6)
163 | panda2 = Panda("Spot", 3)
164 | panda1.interact_with(panda2)
165 |

The following code would not, however. Do you see why?

166 |
pandey = Panda("Pandeybear", 6)
167 | bugs = Rabbit("Bugs Bunny", 2)
168 | bugs.interact_with(pandey)
169 |

The object on the left-hand side of the dot notation is of type Rabbit and there is no interact_with defined on Rabbit, so the original Animal method definition will be used instead.

170 | 171 |
172 | ➡️ Next up: Exercise: Inheritance 173 |
174 | {% endblock %} 175 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/7-exercise-inheritance.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: Inheritance{% endblock %} 3 | {% block content %} 4 |

Exercise: Inheritance

5 |

These exercises are all about overriding base classes. In the first two exercises, you'll override just the class variables. In the last two, you'll also override methods.

6 | 7 |

Exercise: Animal Subclasses

8 |

Finish the two subclasses Sloth and Cat so that they both inherit from Animal and override class variables according to the values in their doctests.

9 |

📺 Need help with this one? Watch this video walkthrough of a partial solution. Then try it out yourself and apply what you learned to the next exercise.

10 | class Animal: 11 | species_name = "Animal" 12 | scientific_name = "Animalia" 13 | play_multiplier = 2 14 | interact_increment = 1 15 | 16 | def __init__(self, name, age=0): 17 | self.name = name 18 | self.age = age 19 | self.calories_eaten = 0 20 | self.happiness = 0 21 | 22 | def play(self, num_hours): 23 | self.happiness += (num_hours * self.play_multiplier) 24 | print("WHEEE PLAY TIME!") 25 | 26 | def eat(self, food): 27 | self.calories_eaten += food.calories 28 | print(f"Om nom nom yummy {food.name}") 29 | if self.calories_eaten > self.calories_needed: 30 | self.happiness -= 1 31 | print("Ugh so full") 32 | 33 | def interact_with(self, animal2): 34 | self.happiness += self.interact_increment 35 | print(f"Yay happy fun time with {animal2.name}") 36 | 37 | 38 | class Sloth: # <- FINISH THIS LINE 39 | """ 40 | >>> Sloth.species_name 41 | "Hoffmann's two-toed sloth" 42 | >>> Sloth.scientific_name 43 | 'Choloepus hoffmanni' 44 | >>> Sloth.calories_needed 45 | 680 46 | >>> buttercup = Sloth('Buttercup', 27) 47 | >>> buttercup.name 48 | 'Buttercup' 49 | >>> buttercup.age 50 | 27 51 | """ 52 | # YOUR CODE HERE 53 | 54 | 55 | class Cat: # <- FINISH THIS LINE 56 | """ 57 | >>> Cat.species_name 58 | 'Domestic cat' 59 | >>> Cat.scientific_name 60 | 'Felis silvestris catus' 61 | >>> Cat.calories_needed 62 | 200 63 | >>> jackson = Cat("Jackson", 8) 64 | >>> jackson.name 65 | 'Jackson' 66 | >>> jackson.age 67 | 8 68 | """ 69 | # YOUR CODE HERE 70 |

Exercise: LearnableContent subclasses

71 |

The LearnableContent class below is based on an actual class from the Khan Academy Python codebase. Finish the implementation of the Article and Video subclasses to properly inherit from LearnableContent and override class variables according to the values in the doctests.

72 | class LearnableContent: 73 | """A base class for specific kinds of learnable content. 74 | All kinds have title and author attributes, 75 | but each kind may have additional attributes. 76 | """ 77 | license = "Creative Commons" 78 | 79 | def __init__(self, title, author): 80 | self.title = title 81 | self.author = author 82 | 83 | def get_heading(self): 84 | return f"{self.title} by {self.author}" 85 | 86 | class Video: # <- FINISH THIS LINE 87 | """ 88 | >>> Video.license 89 | 'YouTube Standard License' 90 | >>> dna_vid = Video('DNA', 'Megan') 91 | >>> dna_vid.title 92 | 'DNA' 93 | >>> dna_vid.author 94 | 'Megan' 95 | >>> dna_vid.get_heading() 96 | 'DNA by Megan' 97 | """ 98 | # YOUR CODE HERE 99 | 100 | 101 | class Article: # <- FINISH THIS LINE 102 | """ 103 | >>> Article.license 104 | 'CC-BY-NC-SA' 105 | >>> water_article = Article('Water phases', 'Lauren') 106 | >>> water_article.title 107 | 'Water phases' 108 | >>> water_article.author 109 | 'Lauren' 110 | >>> water_article.get_heading() 111 | 'Water phases by Lauren' 112 | """ 113 | # YOUR CODE HERE 114 | 115 |

Exercise: Boss methods

116 |

The Character class below represents a character in a video game. Finish implementing the methods in Boss so that they override the base class methods and behave according to the comments and doctests.

117 |

📺 Need help with this one? Watch this video walkthrough of a partial solution. Then try it out yourself and apply what you learned to the next exercise.

118 | class Character: 119 | """ Represents a character in a video game, 120 | tracking their name and health. 121 | 122 | >>> player = Character("Mario") 123 | >>> player.name 124 | 'Mario' 125 | >>> player.health 126 | 100 127 | >>> player.damage(10) 128 | >>> player.health 129 | 90 130 | >>> player.boost(5) 131 | >>> player.health 132 | 95 133 | """ 134 | def __init__(self, name): 135 | self.name = name 136 | self.health = 100 137 | 138 | def damage(self, amount): 139 | self.health -= amount 140 | 141 | def boost(self, amount): 142 | self.health += amount 143 | 144 | 145 | class Boss(Character): 146 | """ Tracks a Boss character in a video game. 147 | 148 | >>> mx_boss = Boss("Mx Boss Person") 149 | >>> mx_boss.damage(100) 150 | >>> mx_boss.health 151 | 99 152 | >>> mx_boss.damage(10) 153 | >>> mx_boss.health 154 | 98 155 | >>> mx_boss.boost(1) 156 | >>> mx_boss.health 157 | 100 158 | """ 159 | 160 | def damage(self, amount): 161 | # Bosses ignore the amount and instead 162 | # always receive 1 unit of damage to their health 163 | # YOUR CODE HERE 164 | 165 | def boost(self, amount): 166 | # Bosses always receive twice the amount of boost to their health 167 | # YOUR CODE HERE 168 | 169 | 170 |

Exercise: Clothing methods

171 |

The Clothing class below represents pieces of clothing in a closet, and KidsClothing inherits from it. Write a clean method that overrides the base class method according to the comment and the doctests.

172 | class Clothing: 173 | """ Represents clothing in a closet, 174 | tracking the color, category, and clean/dirty state. 175 | 176 | >>> blue_shirt = Clothing("shirt", "blue") 177 | >>> blue_shirt.category 178 | 'shirt' 179 | >>> blue_shirt.color 180 | 'blue' 181 | >>> blue_shirt.is_clean 182 | True 183 | >>> blue_shirt.wear() 184 | >>> blue_shirt.is_clean 185 | False 186 | >>> blue_shirt.clean() 187 | >>> blue_shirt.is_clean 188 | True 189 | """ 190 | 191 | def __init__(self, category, color): 192 | self.category = category 193 | self.color = color 194 | self.is_clean = True 195 | 196 | def wear(self): 197 | self.is_clean = False 198 | 199 | def clean(self): 200 | self.is_clean = True 201 | 202 | class KidsClothing(Clothing): 203 | """ 204 | >>> onesie = KidsClothing("onesie", "polka dots") 205 | >>> onesie.wear() 206 | >>> onesie.is_clean 207 | False 208 | >>> onesie.clean() 209 | >>> onesie.is_clean 210 | False 211 | >>> dress = KidsClothing("dress", "rainbow") 212 | >>> dress.clean() 213 | >>> dress.is_clean 214 | True 215 | >>> dress.wear() 216 | >>> dress.is_clean 217 | False 218 | >>> dress.clean() 219 | >>> dress.is_clean 220 | False 221 | """ 222 | 223 | # Override the clean() method 224 | # so that kids clothing always stays dirty! 225 | # YOUR CODE HERE 226 | 227 |
228 | ➡️ Next up: More on inheritance 229 |
230 | {% endblock %} 231 | {% block footer_scripts %} 232 | {% include "_code-exercise-script.html" %} 233 | {% endblock %} 234 | -------------------------------------------------------------------------------- /content/4-object-oriented-programming/8-more-on-inheritance.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}More on inheritance{% endblock %} 3 | {% block content %} 4 |

More on inheritance

5 |

Using super()

6 |

Sometimes we want to be able to call the original method definition from inside the overridden method definition. That is possible using super(), a function that can delegate method calls to the parent class.

7 |

For example, this Lion class has an eat method that only calls the original eat if the meal is meat:

8 |
class Lion(Animal):
  9 |     species_name = "Lion"
 10 |     scientific_name = "Panthera"
 11 |     calories_needed = 3000
 12 | 
 13 |     def eat(self, meal):
 14 |         if meal.kind == "meat":
 15 |             super().eat(meal)
16 |

Since Lion inherits from Animal, that line of code will call the definition of eat inside Animal instead, but pass in the Lion object as the self.

17 |

We'd call it the same way as we usually would:

18 |
bones = Meal("Bones", "meat")
 19 | mufasa = Lion("Mufasa", 10)
 20 | mufasa.eat(bones)
21 |

We could have also written this code instead to achieve the same result:

22 |
def eat(self, food):
 23 |     if food.kind == "meat":
 24 |         Animal.eat(self, food)
25 |

However, the great thing about super() is that it both keeps track of the parent class and takes care of passing in self. Super convenient!

26 |

Using super() with __init__

27 |

A very common use of super() is to call the __init__. Many times, a subclass wants to initialize all the original instance variables of the base class, but then additionally wants to set a few instance variables specific to its needs.

28 |

For example, consider this Elephant class:

29 |
class Elephant(Animal):
 30 |     species_name = "Elephant"
 31 |     scientific_name = "Loxodonta"
 32 |     calories_needed = 8000
 33 | 
 34 |     def __init__(self, name, age=0):
 35 |         super().__init__(name, age)
 36 |         if age < 1:
 37 |             self.calories_needed = 1000
 38 |         elif age < 5:
 39 |             self.calories_needed = 3000
40 |

That __init__ method first calls the original __init__ from Animal and then it conditionally overrides the calories_needed instance variable in the case of young elephants.

41 |

We can create an Elephant object in the usual manner:

42 |
elly = Elephant("Ellie", 3)
 43 | print(elly.calories_needed)
44 | 45 | 46 |
47 |
48 |

🧠 Check Your Understanding

49 |
50 |
51 |

What value would that code display?

52 |
53 | 54 |
55 | 58 | 61 |
62 | 63 | 64 |
65 |
66 |
67 |
68 | 69 |

Layers of inheritance

70 |

Every Python 3 class actually implicitly inherits from the global built-in object class.

71 |

Tree diagram with object class at the top, then Animal, then specific animal subclasses

72 |

We could have written our Animal class to explicitly inherit from it, like so:

73 |
class Animal(object):
74 |

But Python 3 assumes that any class without a base class inherits from object, so we usually don't bother with the extra code.

75 |

The object class includes a few default methods that we can use on any class, including a default implementation of __init__ that doesn't do much of anything at all - but it does mean that we can write a class without __init__ and won't run into errors when creating new instances.

76 |

We can also add more layers of inheritance ourselves, if we realize it would be sensible to have a deeper hierarchy of classes.

77 |

Tree diagram with object class, Animal class, then Herbivore and Carnivore class, with animal subclasses under them

78 |

To add that new layer of classes, we just define the new classes:

79 |
class Herbivore(Animal):
 80 | 
 81 |     def eat(self, meal):
 82 |         if meal.kind == "meat":
 83 |             self.happiness -= 5
 84 |         else:
 85 |             super().eat(meal)
 86 | 
 87 | class Carnivore(Animal):
 88 | 
 89 |     def eat(self, meal):
 90 |         if meal.kind == "meat":
 91 |             super().eat(meal)
92 |

Notice that these subclasses only override a single method, eat.

93 |

Then we change the classes that are currently inheriting from Animal to instead inherit from either Herbivore or Carnivore:

94 |
class Rabbit(Herbivore):
 95 | class Panda(Herbivore):
 96 | class Elephant(Herbivore):
 97 | 
 98 | class Vulture(Carnivore):
 99 | class Lion(Carnivore):
100 |

For a class that represents an omnivorous animal, we could keep its parent class as Animal and not change it to one of the new subclasses.

101 |

Multiple inheritance

102 |

This is where things start to get a little wild. A class can actually inherit from multiple other classes in Python.

103 |

For example, if Meal has a subclass of Prey (since, in the circle of life, animals can become meals!), an animal like Rabbit could inherit from both Prey and Herbivore. Poor rabbits!

104 |

Here's the Prey class:

105 |
class Prey(Meal):
106 |     kind = "meat"
107 |     calories = 200
108 |

To inherit from that class as well, we add it to the class names inside the parentheses:

109 |
class Rabbit(Herbivore, Prey):
110 |

We could even add another subclass to Animal to represent predators, making it so that interacting with a prey animal instantly turns into mealtime.

111 |
class Predator(Animal):
112 | 
113 |     def interact_with(self, other):
114 |         if other.kind == "meat":
115 |             self.eat(other)
116 |             print("om nom nom, I'm a predator")
117 |         else:
118 |             super().interact_with(other)
119 |

We once again would need to modify the class names in the parentheses to make a subclass inherit from Predator as well:

120 |
class Lion(Carnivore, Predator):
121 |

Python can look for attributes in any of the parent classes. It first looks in the current class, then looks at the parent class, then looks at that class's parent class, etc.

122 |
r = Rabbit("Peter", 4)           # Animal __init__
123 | r.play()                         # Animal method
124 | r.kind                           # Prey class variable
125 | r.eat(Food("carrot", "veggies")) # Herbivore method
126 | l = Lion("Scar", 12)             # Animal __init__
127 | l.eat(Food("zazu", "meat"))      # Carnivore method
128 | l.interact_with(r)               # Predator method
129 |

Actually, the way that it finds attributes is even more complicated than that, since it will also look in sibling classes. We won't dive into that here, but if you're curious, search the web for "Python method resolution order (MRO").

130 |

Too much inheritance?

131 |

Inheritance is a great way to avoid repeated code and express the relationships between objects in your programs. However, be careful not to over-use it or take it too far. It can lead to confusing code with surprising results when there are multiple places overriding the same method.

132 |

A good strategy is to start with as little inheritance as possible, perhaps with a single base class (besides object), and then only add more levels of inheritance when it's becoming painful not to add them.

133 |

Another good idea is to use a "mix-in" approach to multiple inheritance: additional base classes should only add new methods, not override methods. That way, you don't have to worry about the order in which Python will look for methods on the base classes.

134 | 135 |
136 | ➡️ Next up: Exercise: More on inheritance 137 |
138 | {% endblock %} 139 | 140 | {% block footer_scripts %} 141 | {% include "_quiz-js-include.html" %} 142 | {% endblock %} -------------------------------------------------------------------------------- /content/4-object-oriented-programming/9-exercise-more-on-inheritance.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Exercise: More on inheritance{% endblock %} 3 | {% block content %} 4 |

Exercise: More on inheritance

5 |

In these four exercises, you'll practice different ways to reuse inherited code from parent classes. Hint: expect to use super() a lot!

6 | 7 |

Exercise: Playful kitty cat

8 |

Modify the Cat initializer method so that a cat that's less than 1 years old has a play_multiplier of 6, and run the doctests when you're done.

9 | class Animal: 10 | species_name = "Animal" 11 | scientific_name = "Animalia" 12 | play_multiplier = 2 13 | interact_increment = 1 14 | 15 | def __init__(self, name, age=0): 16 | self.name = name 17 | self.age = age 18 | self.calories_eaten = 0 19 | self.happiness = 0 20 | 21 | def play(self, num_hours): 22 | self.happiness += (num_hours * self.play_multiplier) 23 | print("WHEEE PLAY TIME!") 24 | 25 | def eat(self, food): 26 | self.calories_eaten += food.calories 27 | print(f"Om nom nom yummy {food.name}") 28 | if self.calories_eaten > self.calories_needed: 29 | self.happiness -= 1 30 | print("Ugh so full") 31 | 32 | def interact_with(self, animal2): 33 | self.happiness += self.interact_increment 34 | print(f"Yay happy fun time with {animal2.name}") 35 | 36 | class Cat(Animal): 37 | """ 38 | >>> adult = Cat("Winston", 12) 39 | >>> adult.name 40 | 'Winston' 41 | >>> adult.age 42 | 12 43 | >>> adult.play_multiplier 44 | 3 45 | >>> kitty = Cat("Kurty", 0.5) 46 | >>> kitty.name 47 | 'Kurty' 48 | >>> kitty.age 49 | 0.5 50 | >>> kitty.play_multiplier 51 | 6 52 | """ 53 | species_name = "Domestic cat" 54 | scientific_name = "Felis silvestris catus" 55 | calories_needed = 200 56 | play_multiplier = 3 57 | 58 | def __init__(self, name, age): 59 | # Call the super class to set name and age 60 | # If age is less than 1, set play multiplier to 6 61 | # YOUR CODE HERE 62 | 63 | 64 |

Exercise: Weighty dogs

65 |

The Dog class in the code below accepts a third argument in the __init__ to set the weight. Implement the method so that it first calls the __init__ of the parent class, then sets the weight attribute, then sets calories_needed to 20 times the weight. Run the doctests when done.

66 | class Animal: 67 | species_name = "Animal" 68 | scientific_name = "Animalia" 69 | play_multiplier = 2 70 | interact_increment = 1 71 | 72 | def __init__(self, name, age=0): 73 | self.name = name 74 | self.age = age 75 | self.calories_eaten = 0 76 | self.happiness = 0 77 | 78 | def play(self, num_hours): 79 | self.happiness += (num_hours * self.play_multiplier) 80 | print("WHEEE PLAY TIME!") 81 | 82 | def eat(self, food): 83 | self.calories_eaten += food.calories 84 | print(f"Om nom nom yummy {food.name}") 85 | if self.calories_eaten > self.calories_needed: 86 | self.happiness -= 1 87 | print("Ugh so full") 88 | 89 | def interact_with(self, animal2): 90 | self.happiness += self.interact_increment 91 | print(f"Yay happy fun time with {animal2.name}") 92 | 93 | 94 | class Dog(Animal): 95 | """ 96 | >>> spot = Dog("Spot", 5, 20) 97 | >>> spot.name 98 | 'Spot' 99 | >>> spot.age 100 | 5 101 | >>> spot.weight 102 | 20 103 | >>> spot.calories_needed 104 | 400 105 | >>> puppy = Dog("Poppy", 1, 7) 106 | >>> puppy.name 107 | 'Poppy' 108 | >>> puppy.age 109 | 1 110 | >>> puppy.weight 111 | 7 112 | >>> puppy.calories_needed 113 | 140 114 | """ 115 | species_name = "Domestic dog" 116 | scientific_name = "Canis lupus familiaris" 117 | calories_needed = 200 118 | 119 | def __init__(self, name, age, weight): 120 | # Call the super class to set name and age 121 | # Set the weight attribute 122 | # Set calories_needed to 20x the weight 123 | # YOUR CODE HERE 124 | 125 |

Exercise: Cats have 9 lives?

126 |

The program below contains a Pet class and a partially implemented Cat class that inherits from it. Read through the current code to understand what it's doing, then implement the Cat methods according to the comments and doctests.

127 | class Pet(): 128 | def __init__(self, name, owner): 129 | self.is_alive = True 130 | self.name = name 131 | self.owner = owner 132 | 133 | def eat(self, thing): 134 | print(self.name + " ate a " + str(thing) + "!") 135 | 136 | def talk(self): 137 | print(self.name) 138 | 139 | class Cat(Pet): 140 | """ 141 | >>> lizzie = Cat("Lizzie", "Hunter") 142 | >>> lizzie.name 143 | 'Lizzie' 144 | >>> lizzie.owner 145 | 'Hunter' 146 | >>> lizzie.lives 147 | 9 148 | >>> lizzie.is_alive 149 | True 150 | >>> lizzie.talk() 151 | Lizzie says meow! 152 | >>> lizzie.lose_life() 153 | >>> lizzie.lose_life() 154 | >>> lizzie.lose_life() 155 | >>> lizzie.lose_life() 156 | >>> lizzie.lose_life() 157 | >>> lizzie.lives 158 | 4 159 | >>> lizzie.lose_life() 160 | >>> lizzie.lose_life() 161 | >>> lizzie.lose_life() 162 | >>> lizzie.lose_life() 163 | >>> lizzie.lives 164 | 0 165 | >>> lizzie.is_alive 166 | False 167 | """ 168 | def __init__(self, name, owner, lives=9): 169 | # Call super class to set name and owner 170 | # Set lives attribute 171 | # YOUR CODE HERE 172 | 173 | def talk(self): 174 | # Print out '<name> says meow!' 175 | # YOUR CODE HERE 176 | 177 | def lose_life(self): 178 | # Decrement a cat's life by 1 179 | # When lives reaches zero, set is_alive to False 180 | # YOUR CODE HERE 181 | 182 | 183 |

Exercise: Herbivores and carnivores

184 |

The program below includes classes you've seen before: Animal, Herbivore, and Carnivore. It also includes 2 new classes, Zebra and Hyena, that don't yet inherit from the correct parent class. Read through their doctests to determine what their parent class should be, and then change their class header as needed. The doctests should all pass if you've made the right change.

185 | class Animal: 186 | species_name = "Animal" 187 | scientific_name = "Animalia" 188 | play_multiplier = 2 189 | interact_increment = 1 190 | 191 | def __init__(self, name, age=0): 192 | self.name = name 193 | self.age = age 194 | self.calories_eaten = 0 195 | self.happiness = 0 196 | 197 | def play(self, num_hours): 198 | self.happiness += (num_hours * self.play_multiplier) 199 | print("WHEEE PLAY TIME!") 200 | 201 | def eat(self, meal): 202 | self.calories_eaten += meal.calories 203 | print(f"Om nom nom yummy {meal.name}") 204 | if self.calories_eaten > self.calories_needed: 205 | self.happiness -= 1 206 | print("Ugh so full") 207 | 208 | def interact_with(self, animal2): 209 | self.happiness += self.interact_increment 210 | print(f"Yay happy fun time with {animal2.name}") 211 | 212 | class Herbivore(Animal): 213 | 214 | def eat(self, meal): 215 | if meal.kind == "meat": 216 | self.happiness -= 5 217 | else: 218 | super().eat(meal) 219 | 220 | class Carnivore(Animal): 221 | 222 | def eat(self, meal): 223 | if meal.kind == "meat": 224 | super().eat(meal) 225 | 226 | class Meal(): 227 | 228 | def __init__(self, name, kind, calories): 229 | self.name = name 230 | self.kind = kind 231 | self.calories = calories 232 | 233 | class Zebra(Animal): 234 | """ 235 | >>> zebby = Zebra("Zebby", 12) 236 | >>> zebby.play(2) 237 | WHEEE PLAY TIME! 238 | >>> zebby.happiness 239 | 4 240 | >>> zebby.eat(Meal("Broccoli", "vegetable", 100)) 241 | Om nom nom yummy Broccoli 242 | >>> zebby.calories_eaten 243 | 100 244 | >>> zebby.eat(Meal("Beef", "meat", 300)) 245 | >>> zebby.happiness 246 | -1 247 | >>> zebby.calories_eaten 248 | 100 249 | """ 250 | species_name = "Common Zebra" 251 | scientific_name = "Equus quagga" 252 | calories_needed = 15000 253 | 254 | class Hyena(Animal): 255 | """ 256 | >>> helen = Hyena("Helen", 12) 257 | >>> helen.play(2) 258 | WHEEE PLAY TIME! 259 | >>> helen.happiness 260 | 4 261 | >>> helen.eat(Meal("Carrion", "meat", 300)) 262 | Om nom nom yummy Carrion 263 | >>> helen.calories_eaten 264 | 300 265 | >>> helen.happiness 266 | 4 267 | >>> helen.eat(Meal("Broccoli", "vegetable", 100)) 268 | >>> helen.calories_eaten 269 | 300 270 | >>> helen.happiness 271 | 4 272 | """ 273 | species_name = "Striped Hyena" 274 | scientific_name = "Hyaena hyaena" 275 | calories_needed = 1000 276 | 277 |

Exercise: Noisy dog

278 |

The program below includes three classes: Pet, Dog, and NoisyDog. Fix NoisyDog so that it inherits from the correct parent class, then override its talk method as described in the comments and doctests.

279 | class Pet(): 280 | def __init__(self, name, owner): 281 | self.is_alive = True 282 | self.name = name 283 | self.owner = owner 284 | 285 | def eat(self, thing): 286 | print(self.name + " ate a " + str(thing) + "!") 287 | 288 | def talk(self): 289 | print(self.name) 290 | 291 | class Dog(Pet): 292 | """ 293 | >>> cooper = Dog("Cooper", "Tinu") 294 | >>> cooper.name 295 | 'Cooper' 296 | >>> cooper.owner 297 | 'Tinu' 298 | >>> cooper.talk() 299 | Cooper says BARK! 300 | """ 301 | 302 | def talk(self): 303 | print(f"{self.name} says BARK!") 304 | 305 | class NoisyDog(Pet): 306 | """ 307 | >>> roxy = NoisyDog("Roxy", "Joe") 308 | >>> roxy.name 309 | 'Roxy' 310 | >>> roxy.owner 311 | 'Joe' 312 | >>> roxy.talk() 313 | Roxy says BARK! 314 | Roxy says BARK! 315 | Roxy says BARK! 316 | """ 317 | 318 | def talk(self): 319 | # Make it so that noisy dogs say the same thing, 320 | # but always say it three times! 321 | # Use super() to call the relevant superclass method 322 | # YOUR CODE HERE 323 | 324 |
325 | ➡️ Next up: Composition 326 |
327 | {% endblock %} 328 | 329 | {% block footer_scripts %} 330 | {% include "_code-exercise-script.html" %} 331 | {% endblock %} 332 | -------------------------------------------------------------------------------- /content/_code-exercise-script.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /content/_quiz-js-include.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /content/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/favicon.ico -------------------------------------------------------------------------------- /content/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /content/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /content/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/apple-touch-icon.png -------------------------------------------------------------------------------- /content/images/callexpression.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/callexpression.png -------------------------------------------------------------------------------- /content/images/callexpression_nested.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/callexpression_nested.png -------------------------------------------------------------------------------- /content/images/callexpression_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/callexpression_tree.png -------------------------------------------------------------------------------- /content/images/classes-chocolate-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/classes-chocolate-shop.png -------------------------------------------------------------------------------- /content/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/favicon-16x16.png -------------------------------------------------------------------------------- /content/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/favicon-32x32.png -------------------------------------------------------------------------------- /content/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/favicon.ico -------------------------------------------------------------------------------- /content/images/inheritance-animal-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/inheritance-animal-diagram.png -------------------------------------------------------------------------------- /content/images/inheritance-animal-pairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/inheritance-animal-pairs.png -------------------------------------------------------------------------------- /content/images/more-on-functions-call-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/more-on-functions-call-error.png -------------------------------------------------------------------------------- /content/images/more-on-functions-call-repl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/more-on-functions-call-repl.png -------------------------------------------------------------------------------- /content/images/more-on-inheritance-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/more-on-inheritance-layers.png -------------------------------------------------------------------------------- /content/images/more-on-inheritance-object-base-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/more-on-inheritance-object-base-class.png -------------------------------------------------------------------------------- /content/images/object-oriented-programming-account-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/object-oriented-programming-account-transfer.png -------------------------------------------------------------------------------- /content/images/object-oriented-programming-account-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/object-oriented-programming-account-types.png -------------------------------------------------------------------------------- /content/images/pamelafox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/pamelafox.jpg -------------------------------------------------------------------------------- /content/images/randomness-pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/randomness-pixels.png -------------------------------------------------------------------------------- /content/images/user-input.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/images/user-input.gif -------------------------------------------------------------------------------- /content/index.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Course Overview{% endblock %} 3 | {% block content %} 4 | 5 |
6 |

Course overview

7 |

Proficient Python is a free online course designed to help you build real confidence in Python — whether you're just getting started or reinforcing your foundations. Through a blend of interactive articles, hands-on exercises, and real projects, you'll develop a solid understanding of the Python language.

8 | 9 |

Ready to get started? Jump into the first unit!

10 |
11 | 12 |
13 |

What you'll learn

14 |

The course covers all the core topics needed to write useful Python programs, including:

15 | 23 |
24 | 25 |
26 |

How you'll learn

27 |

This course offers multiple learning modes to support different learning styles:

28 | 38 |
39 | 40 |
41 |

Getting help

42 |

While this course is designed to be as self-guided and supportive as possible, questions are totally expected! Here's how you can get help along the way:

43 | 44 | 64 |
65 | 66 |
67 |

Teaching resources

68 | 69 |

You are welcome to use this course in your own teaching!

70 | 71 |

This course is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. You are free to share and adapt the content for non-commercial purposes, as long as you provide appropriate credit, indicate if changes were made, and distribute your contributions under the same license.

72 |

73 | Check out the lectures page for slides to complement the course content. 74 | If you have any suggestions for improvements, please open an issue or start a discussion on GitHub. 75 |

76 | 77 |
78 | 79 |
80 |

About the author

81 | 82 | Pamela Fox 83 | 84 |

Pamela Fox is the creator of the course. She originally created the course for the Uplimit learning platform, where she taught it along with Murtaza Ali. 85 | The course was inspired by her experience teaching Python for UC Berkeley CS61A, the first course in the Computer Science curriculum.

86 | 87 |

Before that, she developed interactive programming courses for Khan Academy, covering topics like JavaScript, HTML/CSS, and SQL — helping millions of learners get their start in coding.

88 | 89 |

Today, Pamela works as a Python Cloud Advocate at Microsoft, where she focuses on making programming and cloud technologies more accessible to everyone through tutorials, talks, and open-source tools.

90 |
91 | 92 | {% endblock %} -------------------------------------------------------------------------------- /content/lectures/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/examples/__init__.py -------------------------------------------------------------------------------- /content/lectures/examples/fox.py: -------------------------------------------------------------------------------- 1 | class Fox: 2 | species_name = 'Vulpes vulpes' 3 | 4 | def __init__(self, name): 5 | self.name = name 6 | self.happiness = 0 7 | 8 | def say(self): 9 | return f'{self.name} says Wa-pa-pa-pa-pa-pow' 10 | 11 | def play(self, num_hours): 12 | self.happiness += num_hours 13 | 14 | -------------------------------------------------------------------------------- /content/lectures/examples/input_number.py: -------------------------------------------------------------------------------- 1 | def input_number(message): 2 | while True: 3 | try: 4 | user_input = int(input(message)) 5 | except ValueError: 6 | print("Your answer must be a number. Try again.") 7 | continue 8 | else: 9 | return user_input 10 | -------------------------------------------------------------------------------- /content/lectures/examples/input_number_test.py: -------------------------------------------------------------------------------- 1 | from .input_number import input_number 2 | 3 | def fake_input(msg): 4 | return 5 5 | 6 | def test_input_int(monkeypatch): 7 | monkeypatch.setattr('builtins.input', fake_input) 8 | 9 | assert input_number('Enter num') == 5 10 | 11 | 12 | 13 | 14 | def test_input_str(monkeypatch, capsys): 15 | answers = iter(['none', '50']) 16 | monkeypatch.setattr('builtins.input', lambda msg: next(answers)) 17 | 18 | assert input_number('Enter num') == 50 19 | captured = capsys.readouterr() 20 | assert captured.out == 'Your answer must be a number. Try again.\n' -------------------------------------------------------------------------------- /content/lectures/examples/quiz.py: -------------------------------------------------------------------------------- 1 | class Answer: 2 | def is_correct(self): 3 | return False 4 | 5 | class FreeTextAnswer(Answer): 6 | """ 7 | >>> ans1 = FreeTextAnswer("Milkweed", False) 8 | >>> ans1.is_correct("milkweed") 9 | True 10 | >>> ans1.is_correct("thistle") 11 | False 12 | >>> ans1.display() 13 | Type your answer in (don't worry about capitalization): 14 | >>> ans2 = FreeTextAnswer("Armeria Maritima", True) 15 | >>> ans2.is_correct("armeria maritima") 16 | False 17 | >>> ans2.is_correct("Armeria Maritima") 18 | True 19 | >>> ans2.display() 20 | Type your answer in (capitalization matters!): 21 | """ 22 | def __init__(self, correct_answer, case_sensitive): 23 | # YOUR CODE HERE 24 | self.correct_answer = correct_answer 25 | self.case_sensitive = case_sensitive 26 | 27 | def is_correct(self, user_answer): 28 | # YOUR CODE HERE 29 | if not self.case_sensitive: 30 | return user_answer.lower() == self.correct_answer.lower() 31 | return user_answer == self.correct_answer 32 | 33 | def display(self): 34 | # YOUR CODE HERE 35 | if self.case_sensitive: 36 | print("Type your answer in (capitalization matters!):") 37 | else: 38 | print("Type your answer in (don't worry about capitalization):") 39 | 40 | 41 | class MultipleChoiceAnswer(Answer): 42 | """ 43 | >>> ans = MultipleChoiceAnswer("Dill", ["Milkweed", "Dill", "Thistle"]) 44 | >>> ans.is_correct(1) 45 | False 46 | >>> ans.is_correct(2) 47 | True 48 | >>> ans.is_correct(3) 49 | False 50 | >>> ans.is_correct('2') # Handles strings 51 | True 52 | >>> ans.display() 53 | Type the number corresponding to the correct answer. 54 | 1. Milkweed 55 | 2. Dill 56 | 3. Thistle 57 | """ 58 | # YOUR CODE HERE 59 | def __init__(self, correct_answer, choices): 60 | self.choices = choices 61 | self.correct_answer = correct_answer 62 | 63 | def is_correct(self, user_answer): 64 | """Assumes user answer is number corresponding to answer.""" 65 | return self.choices[int(user_answer) - 1] == self.correct_answer 66 | 67 | def display(self): 68 | print("Type the number corresponding to the correct answer.") 69 | for i in range(0, len(self.choices)): 70 | print(f"{i + 1}. {self.choices[i]}") 71 | 72 | 73 | 74 | class Question: 75 | """ 76 | >>> q1 = Question("What plant do Swallowtail caterpillars eat?", 77 | ... MultipleChoiceAnswer("Dill", ["Milkweed", "Dill", "Thistle"])) 78 | >>> q1.prompt 79 | 'What plant do Swallowtail caterpillars eat?' 80 | >>> q1.answer.correct_answer 81 | 'Dill' 82 | >>> q1.answer_status 83 | 'unanswered' 84 | >>> q2 = Question("What plant do Monarch caterpillars eat?", 85 | ... FreeTextAnswer("Milkweed", False)) 86 | >>> q2.prompt 87 | 'What plant do Monarch caterpillars eat?' 88 | >>> q2.answer.correct_answer 89 | 'Milkweed' 90 | """ 91 | def __init__(self, prompt, answer): 92 | # YOUR CODE HERE 93 | self.prompt = prompt 94 | self.answer = answer 95 | self.answer_status = 'unanswered' 96 | 97 | def display(self): 98 | # YOUR CODE HERE 99 | # Print the question prompt 100 | print(self.prompt) 101 | # Display the answer 102 | self.answer.display() 103 | # Ask the user for input and store their answer 104 | user_answer = input() 105 | # If answer is correct, 106 | # display a message congratulating them 107 | # and mark answered_correctly instance variable as True 108 | # If answer is not correct, 109 | # display a message showing them correct answer 110 | # and mark answered_incorrectly instance variable as False 111 | if self.answer.is_correct(user_answer): 112 | print("You got it!") 113 | self.answer_status = 'correct' 114 | else: 115 | print(f"Sorry, it was: {self.answer.correct_answer}") 116 | self.answer_status = 'incorrect' 117 | 118 | class Quiz: 119 | """ 120 | >>> quiz = Quiz("Butterflies", []) 121 | >>> quiz.name 122 | 'Butterflies' 123 | >>> quiz.questions 124 | [] 125 | >>> q1 = Question("What plant do Swallowtail caterpillars eat?", 126 | ... MultipleChoiceAnswer(["Thistle", "Milkweed", "Dill"], "Dill")) 127 | >>> q2 = Question("What plant do Monarch caterpillars eat?", 128 | ... FreeTextAnswer("Milkweed", False)) 129 | >>> quiz.add_question(q1) 130 | >>> quiz.add_question(q2) 131 | >>> quiz.questions[0].prompt 132 | 'What plant do Swallowtail caterpillars eat?' 133 | >>> quiz.questions[1].prompt 134 | 'What plant do Monarch caterpillars eat?' 135 | """ 136 | def __init__(self, name, questions): 137 | self.name = name 138 | self.questions = questions 139 | 140 | def add_question(self, question): 141 | self.questions.append(question) 142 | 143 | def display(self): 144 | # Display the quiz name 145 | print(f"Welcome to the quiz on {self.name}!") 146 | # Initialize the correct counter to 0 147 | correct_count = 0 148 | # Iterate through the questions 149 | # Display each question 150 | # Increment the correct counter accordingly 151 | for question in self.questions: 152 | question.display() 153 | if question.answer_status == 'correct': 154 | correct_count += 1 155 | # Print the ratio of correct/total 156 | print(f"You got {correct_count}/{len(self.questions)} correct.") 157 | 158 | -------------------------------------------------------------------------------- /content/lectures/examples/scratch.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/examples/scratch.py -------------------------------------------------------------------------------- /content/lectures/examples/sum_pos_scores.py: -------------------------------------------------------------------------------- 1 | def sum_pos_scores(scores): 2 | """ Calculates total score based on list of scores, 3 | but only considers positive scores for the total. 4 | """ 5 | total = 0 6 | for score in scores: 7 | if score > 0: 8 | total += score 9 | return total -------------------------------------------------------------------------------- /content/lectures/examples/sum_pos_scores_test.py: -------------------------------------------------------------------------------- 1 | from .sum_scores import sum_scores 2 | 3 | def test_sum_empty(): 4 | assert sum_scores([]) == 0 5 | 6 | def test_sum_numbers(): 7 | assert sum_scores([8, 9, 7]) == 24 -------------------------------------------------------------------------------- /content/lectures/examples/sum_scores.py: -------------------------------------------------------------------------------- 1 | def sum_scores(scores): 2 | """ Calculates total score based on list of scores. 3 | >>> sum_scores([]) 4 | 0 5 | >>> sum_scores([8, 9, 7]) 6 | 24 7 | """ 8 | total = 0 9 | for score in scores: 10 | total += score 11 | return total -------------------------------------------------------------------------------- /content/lectures/examples/sum_scores_test.py: -------------------------------------------------------------------------------- 1 | from .sum_scores import sum_scores 2 | 3 | def test_sum_empty(): 4 | assert sum_scores([]) == 0 5 | 6 | def test_sum_numbers(): 7 | assert sum_scores([8, 9, 7]) == 24 -------------------------------------------------------------------------------- /content/lectures/examples/test_sum_scores.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from sum_scores import sum_scores 4 | 5 | class TestSumScores(unittest.TestCase): 6 | 7 | def test_sum_empty(self): 8 | self.assertEqual(sum_scores([]), 0) 9 | 10 | def test_sum_numbers(self): 11 | self.assertEqual(sum_scores([8, 9, 7]), 24) 12 | 13 | if __name__ == '__main__': 14 | unittest.main() -------------------------------------------------------------------------------- /content/lectures/examples/texter.py: -------------------------------------------------------------------------------- 1 | from texter import uppercase, lowercase, binarize, clapify, emojify, exclamify, mystery 2 | 3 | # Try these functions in the console and see what they do! 4 | uppercase('ReplIt is so not intuitive') 5 | lowercase('Learning as I go') 6 | clapify('I have a cold') 7 | emojify('I am sick and tired of having a cold') 8 | exclamify('T00 many hidden links') 9 | binarize('I am not a computer') 10 | mystery('Wee Free Men. I am not gonna be a knee.') 11 | 12 | # Examples from class: 13 | uppercase('hi hello') 14 | lowercase('HI HELLO') 15 | clapify('lets all clap') 16 | clapify(uppercase('just do it')) 17 | uppercase('just do it') 18 | clapify('JUST DO IT') 19 | exclamify('i love encanto') 20 | exclamify(clapify(uppercase('just do it'))) 21 | uppercase(clapify('just do it')) 22 | binarize('abc') 23 | clapify(binarize('abc')) 24 | emojify('im sick of cake') 25 | exclamify(emojify('im sick of cake')) 26 | mystery('hello world') 27 | mystery('hello world i am just going to type a lot') -------------------------------------------------------------------------------- /content/lectures/examples/weather.py: -------------------------------------------------------------------------------- 1 | def suggest_supplies(temperature, windspeed, incline): 2 | """ 3 | >>> suggest_supplies(33, 3, 0) 4 | '' 5 | >>> suggest_supplies(33, 3, 5) 6 | 'stick ' 7 | >>> suggest_supplies(33, 6, 0) 8 | 'windbreaker ' 9 | >>> suggest_supplies(17, 0, 0) 10 | 'thermal ' 11 | >>> suggest_supplies(33, 6, 10) 12 | 'windbreaker stick ' 13 | >>> suggest_supplies(17, 0, 10) 14 | 'stick thermal ' 15 | """ 16 | supplies = '' 17 | if windspeed > 5: 18 | supplies += 'windbreaker ' 19 | elif incline > 0: 20 | supplies = 'stick ' 21 | elif temperature < 32: 22 | supplies += 'thermal ' 23 | return supplies 24 | 25 | suggest_supplies(33, 6, 10) -------------------------------------------------------------------------------- /content/lectures/index.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Lectures{% endblock %} 3 | {% block content %} 4 | 5 |

Lectures

6 | 7 |

If you are a teacher using this course for a classroom or online setting, you may find these lecture slides helpful.

8 | 9 |

10 | Each lecture should take about 1 hour to present. 11 | The unit lectures cover at least the first topics in a unit, but don't always cover all the topics in the unit, 12 | as students are expected to work through the rest of the unit on their own. 13 | The bonus lectures cover additional topics that are not covered in the articles. 14 |

15 | 16 |

The slides are written in HTML/CSS using Reveal.JS, so that they can be viewed in a web browser. 17 | Download the original ones from the GitHub repository 18 | if you'd like to edit them. 19 |

20 | 21 | 30 | 31 | {% endblock %} -------------------------------------------------------------------------------- /content/lectures/media/input_commandline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/input_commandline.gif -------------------------------------------------------------------------------- /content/lectures/media/name_value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/name_value.png -------------------------------------------------------------------------------- /content/lectures/media/pixel_grid.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/pixel_grid.psd -------------------------------------------------------------------------------- /content/lectures/media/screenshot_colab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/screenshot_colab.png -------------------------------------------------------------------------------- /content/lectures/media/screenshot_debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/screenshot_debugger.png -------------------------------------------------------------------------------- /content/lectures/media/screenshot_exercise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/screenshot_exercise.png -------------------------------------------------------------------------------- /content/lectures/media/screenshot_githubactions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/screenshot_githubactions.jpg -------------------------------------------------------------------------------- /content/lectures/media/screenshot_pythontutor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/screenshot_pythontutor.png -------------------------------------------------------------------------------- /content/lectures/media/software_testing_pyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pamelafox/proficient-python/cfce0f767da6ca6828c0ed3e0f46577d886aed2b/content/lectures/media/software_testing_pyramid.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | target-version = "py312" 4 | select = ["E", "F", "I", "UP", "A"] 5 | ignore = ["D203"] 6 | 7 | [tool.pytest.ini_options] 8 | testpaths = ["tests"] 9 | pythonpath = ['.'] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2ssg 2 | pytest 3 | playwright 4 | pytest-playwright 5 | axe-playwright-python 6 | ruff -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | python3 -m http.server 8000 --directory build -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing the course 2 | 3 | This directory contains tests for the rendered HTML course content in the `build` folder. 4 | 5 | ## Prerequisites 6 | 7 | Before running the tests, make sure you have installed the required dependencies: 8 | 9 | ```bash 10 | pip install -r requirements.txt 11 | playwright install 12 | ``` 13 | 14 | Then build the course content: 15 | 16 | ```bash 17 | ./build.sh 18 | ``` 19 | 20 | ## Running the Tests 21 | 22 | To run all tests: 23 | 24 | ```bash 25 | pytest 26 | ``` 27 | 28 | To run tests with detailed output: 29 | 30 | ```bash 31 | pytest -v 32 | ``` -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | import socket 4 | import time 5 | from contextlib import closing 6 | from multiprocessing import Process 7 | import subprocess 8 | import sys 9 | 10 | 11 | def wait_for_server_ready(url: str, timeout: float = 10.0, check_interval: float = 0.5) -> bool: 12 | """Make requests to provided url until it responds without error.""" 13 | conn_error = None 14 | for _ in range(int(timeout / check_interval)): 15 | try: 16 | requests.get(url) 17 | except requests.ConnectionError as exc: 18 | time.sleep(check_interval) 19 | conn_error = str(exc) 20 | else: 21 | return True 22 | raise RuntimeError(conn_error) 23 | 24 | 25 | @pytest.fixture(scope="session") 26 | def free_port() -> int: 27 | """Returns a free port for the test server to bind.""" 28 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 29 | s.bind(("", 0)) 30 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 31 | return s.getsockname()[1] 32 | 33 | 34 | def run_server(port: int): 35 | # Use subprocess to run a simple HTTP server (python3 -m http.server 8000 --directory build 36 | command = [sys.executable, "-m", "http.server", 37 | str(port), "--directory", "build"] 38 | subprocess.run(command, check=True) 39 | 40 | 41 | @pytest.fixture() 42 | def live_server_url(free_port: int): 43 | proc = Process(target=run_server, args=(free_port,), daemon=True) 44 | proc.start() 45 | url = f"http://localhost:{free_port}/" 46 | wait_for_server_ready(url, timeout=10.0, check_interval=0.5) 47 | yield url 48 | proc.kill() 49 | -------------------------------------------------------------------------------- /tests/test_e2e.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import Page, expect 2 | from axe_playwright_python.sync_playwright import Axe 3 | from playwright.sync_api import Page 4 | 5 | 6 | def test_index_page(page: Page, live_server_url: str): 7 | """Test that the index page loads correctly.""" 8 | page.goto(f"{live_server_url}index.html") 9 | 10 | # Expand the first unit in the sidebar 11 | page.get_by_role("button", name="Python Fundamentals").click() 12 | page.get_by_role("link", name="Values & Expressions", exact=True).click() 13 | page.locator("code-exercise-element[name=magic-birthday-math]").get_by_label("Run code").click() 14 | expect(page.locator("code-exercise-element[name=magic-birthday-math] ")).not_to_contain_text("Running code") 15 | expect(page.locator("code-exercise-element[name=magic-birthday-math] pre code")).to_contain_text("7") 16 | 17 | # Check inline quiz 18 | page.get_by_role("main").get_by_role("link", name="Nested Call Expressions", exact=True).click() 19 | page.get_by_role("radio", name="Yes, there needs to be an import statement above it.").check() 20 | page.locator("form").filter(has_text="🧠 Check Your Understanding Is any additional code required for the above call").get_by_role("button").click() 21 | expect(page.locator("#content")).to_contain_text("This answer is correct, there needs to be from operator import add before this line of code.") 22 | 23 | # Check code exercise with doctests 24 | page.get_by_role("link", name="Exercise: Functions").click() 25 | page.locator("code-exercise-element[name=lifetime-supply-calculator]").get_by_label("Run tests").click() 26 | expect(page.locator("code-exercise-element[name=lifetime-supply-calculator]")).not_to_contain_text("Running code") 27 | expect(page.locator("code-exercise-element[name=lifetime-supply-calculator]")).to_contain_text("❌ Failed test: calculate_lifetime_supply(99, 1) Expected: 365 Got nothing") 28 | 29 | 30 | def test_next_links(page: Page, live_server_url: str): 31 | """Test that the next links work correctly by following all next links until Project 4.""" 32 | page.goto(f"{live_server_url}index.html") 33 | 34 | # Click on link with text "Jump into the first unit!" 35 | page.get_by_role("link", name="Jump into the first unit!").click() 36 | expect(page).to_have_url(f"{live_server_url}1-python-fundamentals/0-unit-overview.html") 37 | 38 | # Initialize counters for tracking progress 39 | visited_pages = set() 40 | current_url = page.url 41 | visited_pages.add(current_url) 42 | 43 | # Track progress through units 44 | unit_progress = { 45 | "1-python-fundamentals": False, 46 | "2-loops-&-lists": False, 47 | "3-strings-&-dictionaries": False, 48 | "4-object-oriented-programming": False 49 | } 50 | 51 | # Follow next links until we reach Project 4 or hit a cycle 52 | max_clicks = 100 # Safety limit to prevent infinite loops 53 | clicks = 0 54 | 55 | while clicks < max_clicks: 56 | # Check if we've reached Project 4 57 | if "4-object-oriented-programming/12-project-4-oop-quiz.html" in page.url: 58 | print("Successfully reached Project 4!") 59 | # Verify it's the final project 60 | expect(page.locator("h1")).to_contain_text("Project 4:") 61 | break 62 | 63 | # Check which unit we're in 64 | for unit in unit_progress.keys(): 65 | if unit in page.url: 66 | unit_progress[unit] = True 67 | 68 | # Look for next-step section with link 69 | next_link = page.locator("section.next-step").get_by_role("link") 70 | 71 | # If no next link or it's not visible, we've hit a dead end 72 | if next_link.count() == 0: 73 | raise AssertionError(f"No next link found on page {page.url}") 74 | 75 | # Click the next link 76 | with page.expect_navigation(): 77 | next_link.click() 78 | 79 | # Verify navigation worked and we're on a valid page 80 | expect(page.locator("main h1")).to_be_visible() 81 | 82 | # Check if we've already visited this page (cycle detection) 83 | current_url = page.url 84 | if current_url in visited_pages: 85 | raise AssertionError(f"Cycle detected! Revisited page: {current_url}") 86 | 87 | visited_pages.add(current_url) 88 | clicks += 1 89 | print(f"Navigated to: {page.url}") 90 | 91 | # Verify we visited all units 92 | for unit, visited in unit_progress.items(): 93 | assert visited, f"Failed to visit any page in unit {unit}" 94 | 95 | # Make sure we didn't hit the safety limit 96 | assert clicks < max_clicks, f"Hit safety limit of {max_clicks} clicks without reaching Project 4" 97 | 98 | 99 | 100 | def test_a11y(page: Page, live_server_url: str): 101 | axe = Axe() 102 | page.goto(f"{live_server_url}/index.html") 103 | results = axe.run(page) 104 | assert results.violations_count == 0, results.generate_report() -------------------------------------------------------------------------------- /tutor.prompt.yaml: -------------------------------------------------------------------------------- 1 | name: Socratic Python Tutor 2 | description: Acts as a Socratic-style Python tutor that guides users to solutions without giving direct answers. 3 | model: gpt-4o 4 | modelParameters: 5 | temperature: 0.7 6 | messages: 7 | - role: system 8 | content: | 9 | ### ROLE: PYTHON TUTOR ### 10 | You are "PyGuide," an expert Socratic-style Python programming tutor. Your single most important goal is to help me learn and understand Python by guiding me to the answers, NOT by giving them to me directly. You are a teacher, not a code-writer. 11 | 12 | ### CORE DIRECTIVES ### 13 | 1. **NEVER Provide the Final Solution:** Under no circumstances will you provide the complete, final code solution to my problem. [cite_start]This is a strict rule. [cite: 72] 14 | 2. **Guide, Don't Give:** Your entire purpose is to guide me. If I am stuck, you will provide a hint, ask a targeted question, or explain a relevant concept, but you will not do the work for me. 15 | 3. **Explain the "Why":** For every hint, concept, or piece of guidance you provide, you MUST explain the reasoning behind it. For example, if you suggest a function, explain *why* that function is appropriate for this context. [cite_start]This is crucial for my learning. [cite: 107] 16 | 4. **One Step at a Time:** Guide me through the problem-solving process incrementally. Provide one hint or ask one question, and then WAIT for my response. Do not provide multiple steps at once. 17 | 18 | ### METHODOLOGY ### 19 | When I present a problem, you will use the following Socratic method: 20 | 1. **Clarify Understanding:** First, ensure I understand the problem. You might ask me to rephrase the goal in my own words. 21 | 2. [cite_start]**Deconstruct the Problem:** Encourage me to break the problem down into smaller, logical sub-tasks. [cite: 55, 279] For example: "Great, you need to read the file and count the words. What do you think the very first step should be?" 22 | 3. **Provide Conceptual Scaffolding:** If I'm unsure which tool to use, you will introduce the relevant Python concept. 23 | 4. **Ask Leading Questions:** Use questions to prompt my own thinking. 24 | 5. **Error Analysis:** If I write code that is incorrect, do not correct it for me. Instead, help me debug it myself. 25 | 26 | ### INTERACTION STYLE ### 27 | - **Tone:** Patient, encouraging, and supportive. 28 | - **Start:** After this initial setup, begin our first interaction by introducing yourself as PyGuide and asking for my first Python question or problem. 29 | - role: user 30 | content: | 31 | {{input}} 32 | testData: 33 | - input: | 34 | I need to write a Python function that takes a list of names and returns only the names that start with the letter 'A'. 35 | expected: | 36 | Hello! I'm PyGuide, your Python tutor. That's a great problem to work on. 37 | 38 | To get started, let's break it down. You have a list of names, and you need to check each one. What's a common way in Python to look at every single item in a list, one by one? 39 | evaluators: 40 | - name: Response should not contain a for loop 41 | string: 42 | notContains: 'for name in names:' 43 | - name: Response should ask a question 44 | string: 45 | endsWith: '?' 46 | - name: Response should not contain the complete solution code 47 | string: 48 | notContains: 49 | - '.startswith("A")' 50 | - 'return' --------------------------------------------------------------------------------