├── .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 |  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 |
Welcome to your first unit of learning Python!
6 |Our learning goals for this unit are:
7 |Expressions
Variables
Functions
Logical expressions
Conditionals
At the end, you'll combine those skills to make a decision making algorithm.
13 | 14 | 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 |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 |An integer is a number with nothing after the decimal. Examples: 2
, 44
, -3
A float is a number that does have something after the decimal, even if that something is a 0. Floating-point numbers are stored differently in computers and are treated differently in arithmetic calculations, so it's important to know whether you want a float or an integer. Examples: 3.14159
, 4.5
, -2.0
A Boolean is a value that can only store two possible values, either True
or False
. They’ll become very important when we want to be able to make decisions in programs and take different code paths based on whether something is true or false.
A string is a string of text, where that text could be anything allowed by the character encoding. We can get into that later, but for now, you should be able to use a wide range of characters, including non-English characters and even emojis! For example, '¡hola!'
and 'python party 🐍'
are both valid strings. Strings can either be surrounded by single quotes' or "double quotes", as long as it’s the same on both sides.
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 |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.
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.
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 | 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 |Start with the number 7
Multiply by the month of your birth
Subtract 1
Multiply by 13
Add the day of your birth
Add 3
Multiply by 11
Subtract the month of your birth
Subtract the day of your birth
Divide by 10
Add 11
Divide by 100
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 |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 |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.
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:
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 | 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 |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!
📺 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 |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 |🧩 For more guidance, try a Parsons Puzzle version of the problem first.
46 |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
.
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:
Once 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:
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.
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 | 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?
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:
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()
.
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()
.
If you want to remove the digits after the decimal afterwards, you can then use either int()
, round()
, floor()
, or ceil()
.
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 |
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 | 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 |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 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /content/1-python-fundamentals/4-functions.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html.jinja2" %} 2 | {% block title %}Functions{% endblock %} 3 | {% block content %} 4 |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.
2, 10 → pow → 1024
14 |In the pow
expression, there are two arguments of 2 and 10, and a return value of 1024.
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 |The most common way to define functions in Python is with a def
statement.
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 | 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.
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 |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.
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.
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 | 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 | Here's some more buggy code:
110 |def add_two():
111 | return num1 + num2
112 |
113 | result = add_two(2, 4)
114 |
115 |
116 | 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 | 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 |📺 If you'd like, you can watch how I tackled this first exercise in a walkthrough video.
31 |return
statement.
35 | -
, *
, (
, )
.
36 | current_age
, amount_per_day
) as well as literal numbers (100
, 365
).
37 | Fill in the blanks with the appropriate parameters and try running the tests.
41 |return (100 - ______) * (______ * 365)
42 | 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.
return
statement.
66 | -
, *
, (
, )
.
67 | year1
, year2
) as well as literal numbers (365
, 24
, 60
).
68 | Fill in the blank with the appropriate expression and try running the tests.
72 |return (_____________) * 365 * 24 * 60 * 60
73 | 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.
return
statement.
92 | +
operator.
93 | job
, place
, partner
) as well as a literal string ('You will be a '
, ' in '
, ' living with '
, ' .'
).
94 | Fill in the blanks with the appropriate parameters and try running the tests.
98 |return 'You will be a ' + ____ + ' living in ' + ____ + ' with ' + ____ + '.'
99 | 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.
Tip: As the final step in the conversion, the built-in round
function may be helpful.
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.
📺 Need help getting started on this one? Watch me go through a similar problem in this video.
9 | 10 |==
with the and
operator.
15 | father_allele
, mother_allele
) as well as literal strings ("C"
).
16 | Fill in the blanks with the correct comparison and try running the tests.
20 |return father_allele ______ and mother_allele ______
21 | >=
with the and
operator.
45 | age
, residency
) as well as literal numbers (35
, 14
).
46 | Fill in the blanks with the correct comparison and try running the tests.
50 |return ______ and ______
51 | >=
, ==
, with the or
operator.
84 | seafood_type
, days_frozen
) as well as a literal number (7
) and a literal string
("mollusk"
).
85 | Fill in the blanks with the correct comparison and try running the tests.
89 |return ______ or ______
90 | 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 |In Python, conditional statements always start with if
. Here's the basic template:
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.
Here's a simple if
statement that re-assigns a variable based on a condition:
clothing = "shirt"
15 |
16 | if temperature < 32:
17 | clothing = "jacket"
18 |
19 |
20 | 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
. 🤷🏼♀️
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 |
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 | |
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.
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
:
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.
In summary, a conditional statement:
182 |always starts with an if
clause
can have 0 or more elif
clauses
can optionally end with an else
clause
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:
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.
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 |Loops
Ranges
Lists
List mutation
Nested lists
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 | 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 |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 |Implement sum_grid
, a function that returns the sum of all the numbers in grid
, a 2-dimensional list of numbers.
📺 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 |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.
🧩 For more guidance, try a Parsons Puzzle version first.
37 |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 |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 | 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 |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 |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.
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.
📺 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 |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
.
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.
🧩 For more, try a Parsons Puzzle version first.
29 | 30 |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.
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 |The code for the next function is already written, but it has several bugs.
69 |Tips for debugging:
70 |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.
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:
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.)
There's a much simpler way to iterate through a list: the for
loop.
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
.
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 |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 |Implement make_point
, a function that returns a list with the two arguments x
and y
, each rounded to integer values.
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 |Implement average_scores
, a function that returns the average of the provided list scores
.
📺 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 |Implement concatenator
, a function that returns a string made up of all the strings from the provided list items
.
(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 |In these exercises, you'll practice list mutation using both list methods and bracket notation.
6 |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.
last_num = sequence[__]
19 | sequence.______(last_num + 1)
20 | 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.
sequence.________(___________, 'malcolm')
49 | Implement double_time
, a function that doubles each element in the provided list.
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 |Here are a few more helpful methods that can be called on lists:
7 |
11 | Method 12 | |
13 | Description 14 | |
---|---|
17 |
|
19 | Reverses the items of the list, mutating the original list. 20 | |
22 |
|
24 | Sorts the items of the list, mutating the original list. 25 | |
27 |
|
29 | Returns the number of occurrences of item in the list. If item is not found, returns 0. 30 | |
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:
nums.reverse()
36 | That method mutates the original list, so nums
now holds [10, 9, 8, 7, 6, 5, 4, 3, 2, 1].
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:
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
.
scores.sort(reverse=True)
43 | 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 |
49 | Function 50 | |
51 | Description 52 | |
---|---|
55 |
|
57 | Returns a sorted version of the iterable object. Set the |
60 |
|
62 | Returns the minimum value in the iterable object. 63 | |
65 |
|
67 | Returns the maximum value in the iterable object. 68 | |
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:
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 |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.
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 |
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 | 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 |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:
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 | 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 |To iterate through every item in a nested list, we can use a nested for loop: a for
loop inside a for
loop!
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:
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.
If you're ever not sure what your variable is storing, you can always print it out and double check.
136 | 137 | 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 |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 |Strings, string formatting, and string operations
The dictionary data type
Generating randomness programmatically
Working with files (both local and online)
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 | 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 |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.
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 | 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.
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:
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.
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.
One approach is to convert the number to a string, by calling the globally available str()
on it.
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.
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 |
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 | Use f strings to complete these functions that return strings based on the input parameters.
6 | 7 |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 |The next two exercises help you practice using string methods.
10 | 11 |Implement bleep_out
, a function that looks for a particular phrase in a string and returns a new string with that phrase replaced by '***'.
return
statement.
18 | replace
method on string1
, passing in string2
and the literal string "***"
.
19 | 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.
phone_number
into parts, using the split
method, and store the results in a list.
41 | join
method.
42 | The next three exercises help you practice using string slicing.
60 | 61 |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'.
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.
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 |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".
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.
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 |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 |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:
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)
.
Thanks to that function, the code can become much cleaner and concise:
71 |random.choice(yays)
72 | 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 |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.
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 |Images (with extensions such as GIF/JPG/PNG)
Audio (e.g. WAV/MP3)
Video (e.g. MOV/MP4)
Compressed files (e.g. ZIP/RAR)
This is the general 3-step process for handling files in Python:
15 |Open the file
Either read data from file or write data to file
Close the file
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 |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:
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 |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:
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"
).
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.
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:
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 |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 | 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.
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.
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.
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 |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!
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.
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.
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.
Once decoded, we can use string operations on it, such as using split
to turn it into a list of lines.
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 |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 | 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 |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 |Classes
Inheritance
Composition
Polymorphism
At the end, you'll combine those skills to make a quiz on the topic of your choosing.
12 | 13 | 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 |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 |A way to describe a type of complex data, such as a bank account which has an associated account owner, a current balance, a history of deposits/withdrawals, etc.
A way to bundle together data with related functionality. For a bank account, it might need functions for withdrawing money, depositing money, closing the account, transferring between accounts, etc.
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 |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 |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.
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 | 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 |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 |An animal has a mate (another animal of the same class).
An animal has a mother (also another animal of the same class).
An animal has children (multiple animals of the same class).
We can implement all of those examples using instance variables that either store a single object or a list of objects.
11 |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.
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 | 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.
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.
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 |A rabbit is a specific type of animal.
So, the Rabbit
class inherits from Animal
.
Composition is best for representing "has-a" relationships.
51 |A rabbit has a litter of babies.
So, a Rabbit
object has an instance variable containing other Rabbit
objects.
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 | 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 |One of the great benefits of object-oriented programming is polymorphism: different types of objects sharing the same interface.
6 |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:
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:
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.
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 |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:
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?
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.
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.
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 | 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 |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 |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 |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.
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.
📺 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 |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.
🧩 For more guidance, try a Parsons Puzzle version first.
41 | 42 |Finish the implementation of Player
, a class that represents a player in a video game, tracking their name and health.
🧩 For more guidance, try a Parsons Puzzle version first.
69 | 70 |Implement Clothing
, a class that represents pieces of clothing in a closet, tracking their color, category, and clean/dirty state.
🧩 For more guidance, try a Parsons Puzzle version first.
99 | 100 |These exercises will give you practice with class variables and class methods.
6 |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 |📺 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 |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.
Hint: You can use random.choice(list)
to generate a random color from a list of colors.
📺 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 |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! 🐈⬛🐈
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.
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 |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:
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.
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 |Both Elephant
and Rabbit
have the same three class variable names, though the values of their class variables differ.
Elephant
and Rabbit
take the same arguments in their __init__
method, and then initialize the same four instance variables. Their __init__
methods are, in fact, identical.
The eat
and play
methods are identical. The interact_with
methods are nearly identical, but vary in how much the happiness instance variable increases.
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.
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:
You'll also hear the base class referred to as the superclass.
103 |Here's how we could write the Animal
base class:
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 |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:
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:
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 | 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:
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:
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.
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 |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.
📺 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 |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.
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.
📺 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 |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.
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.
For example, this Lion
class has an eat
method that only calls the original eat
if the meal is meat:
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
.
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!
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.
For example, consider this Elephant
class:
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.
We can create an Elephant
object in the usual manner:
elly = Elephant("Ellie", 3)
43 | print(elly.calories_needed)
44 |
45 | Every Python 3 class actually implicitly inherits from the global built-in object
class.
We could have written our Animal
class to explicitly inherit from it, like so:
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.
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.
We can also add more layers of inheritance ourselves, if we realize it would be sensible to have a deeper hierarchy of classes.
77 |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
.
Then we change the classes that are currently inheriting from Animal
to instead inherit from either Herbivore
or Carnivore
:
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.
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!
Here's the Prey
class:
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.
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:
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 |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.
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 | 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 |In these four exercises, you'll practice different ways to reuse inherited code from parent classes. Hint: expect to use super()
a lot!
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.
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.
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.
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.
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.
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 |The course covers all the core topics needed to write useful Python programs, including:
15 |This course offers multiple learning modes to support different learning styles:
28 |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 |🤖 Use AI tools thoughtfully: Tools like ChatGPT or Claude can be helpful if you're stuck, but try not to just ask them for the solution. Instead, share your code and ask:
52 |You may need to even specifically add an instruction like "Do NOT give me the solution." 58 |
59 |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 |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 |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 |