A quick-reference and hands-on guide to the fundamentals of Python programming.
The fastest way to keep what you learn is to teach it. The physicist Richard Feynman's trick was simple: if you can't explain something in plain words, you don't really understand it yet. For each section, work these four steps:
Every exercise is a complete program. Save it as practice.py and run it with python3 practice.py (Python 3.10 or newer, for match). Watch for the 💡 Explain it simply prompts and ✎ Exercise boxes in each section. Hover any code block and click Copy to grab it.
A programming language is a precise, written notation for telling a computer what to do. Python was created by Guido van Rossum and first released in 1991. His central idea was that code is read far more often than it is written, so a language should look as close to plain English as possible. Python keeps its syntax deliberately small, which is why it is so often the first language people learn. It is also a working professional's tool, used for web applications (this site runs on Python), data analysis, scientific computing, machine learning, and automating everyday tasks.
Python is an interpreted language. Rather than translating the whole program into machine code ahead of time, you hand your .py file to the Python interpreter, which reads and carries out the instructions as it goes. (Strictly, it first compiles your code to an internal bytecode, but that happens automatically and invisibly.) This makes the edit-and-run cycle very fast, and you can also type code into the interactive prompt, the REPL, and see each result immediately.
Python is also dynamically typed: you never declare a variable's type, because the type belongs to the value, not the variable, and it is checked while the program runs. Most distinctive of all, Python uses indentation to show structure. Where other languages wrap blocks in curly braces, Python uses the lines' leading spaces, so correctly indented code is not just good style; it is required.
python3 file.py.>>> prompt for trying code line by line..py extension# for single-line and """...""" for multi-line# Single-line comment
"""
Multi-line string / docstring
Often used as a comment block
"""
print("Hello, World!")
Explain to a friend why a Python program breaks if you indent a line by the wrong amount, when in most languages spacing is just cosmetic.
In Python the spaces at the start of a line are the punctuation. They tell Python which lines belong inside an if, a loop, or a function, the job curly braces do elsewhere. Indent a line wrongly and you've literally moved it into (or out of) a block, so Python either can't make sense of it or does something different from what you meant.
A variable is a name that refers to a value, so you can use it and change it later. In Python you create one simply by assigning to it with =, as in age = 30; there is no keyword and no type declaration. It helps to think of a variable as a label tied to a value rather than a box: the value lives in memory, and the name points at it. Reassigning the name moves the label to a different value.
Every value has a data type, which decides what you can do with it. The basic types are int (whole numbers, which in Python can be any size), float (numbers with a decimal point), str (text), bool (True or False, with capital letters), and None, a special value meaning “nothing here”. The built-in type() function tells you the type of any value. Because Python is dynamically typed, the same name may refer to a number now and a string later, although doing so usually makes code harder to follow.
Values can be converted between types with functions named after the target type: int("42") turns text into a number, and str(100) turns a number into text. This matters because Python will not silently mix them; "4" + 2 raises an error rather than guessing what you meant. Python also has a concept of truthiness: in a condition, 0, empty strings, empty collections, and None count as false, and almost everything else counts as true.
=, which creates the variable if it doesn't exist.int("42").name = "Alice" # str
age = 30 # int
price = 9.99 # float
active = True # bool (capital T/F)
nothing = None # NoneType
print(type(name)) # <class 'str'>
print(type(age)) # <class 'int'>
# Multiple assignment
x, y, z = 1, 2, 3
# Type conversion
int("42") # 42
float("3.14") # 3.14
str(100) # "100"
bool(0) # False
| Type | Example | Description |
|---|---|---|
str | "hello" | Text |
int | 42 | Whole numbers |
float | 3.14 | Decimal numbers |
bool | True / False | Boolean values |
None | None | Absence of a value |
list | [1, 2, 3] | Ordered, mutable sequence |
tuple | (1, 2, 3) | Ordered, immutable sequence |
dict | {"key": "val"} | Key-value pairs |
set | {1, 2, 3} | Unordered, unique values |
Create the four variables so the first two lines print as shown, then finish the last line so it converts the text "42" to a number and adds 1.
# TODO: create name ("Ada"), age (36), height (1.65) and is_admin (False)
print(f"{name} is {age} years old and {height} m tall.")
print(f"Admin: {is_admin}")
print(type(height))
print("42" + 1) # TODO: fix this line so it prints 43
name = "Ada"
age = 36
height = 1.65
is_admin = False
print(f"{name} is {age} years old and {height} m tall.")
print(f"Admin: {is_admin}")
print(type(height))
print(int("42") + 1)
Ada is 36 years old and 1.65 m tall. Admin: False <class 'float'> 43
Why does "4" + 2 fail in Python while int("4") + 2 works? Explain it without the word “type”.
"4" in quotes is a piece of text that happens to show the character 4, like a 4 written on a sign. Python won't guess whether you want to do maths or glue text together, so it refuses. int("4") reads the sign and gives you the actual number four, and numbers can be added.
An operator is a symbol or keyword that performs an operation on one or more values, called operands. In a + b, the + is the operator. Any combination of values and operators that produces a result is an expression, and Python evaluates expressions following precedence rules, so ** comes before *, which comes before +, just as in ordinary maths.
Python's arithmetic operators include two kinds of division. / is true division and always gives a float, so 10 / 2 is 5.0. // is floor division, which rounds down to a whole number, and % (modulus) gives the remainder. ** raises to a power. Comparison operators (== != < > <= >=) produce True or False, and they can even be chained, as in 0 < x < 10.
Where many languages use symbols, Python uses English words. The logical operators are and, or, and not. in tests membership, whether a value is inside a string, list, or other collection. is tests identity, whether two names refer to the very same object in memory. That is different from ==, which asks whether two values are equal. The one common use of is is checking x is None.
//, division rounded down to a whole number: 17 // 5 is 3.in / not in, testing whether a value is inside a collection.is asks “same object?”; == asks “same value?”a, b = 10, 3
a + b # 13 — addition
a - b # 7 — subtraction
a * b # 30 — multiplication
a / b # 3.333… — true division (always float)
a // b # 3 — floor division (integer result)
a % b # 1 — modulus (remainder)
a ** b # 1000 — exponentiation
x == y # equal
x != y # not equal
x < y # less than
x > y # greater than
x <= y # less than or equal
x >= y # greater than or equal
a and b # True if both are truthy
a or b # True if either is truthy
not a # inverts the boolean
x is y # True if same object in memory
x is not y # True if different objects
"a" in "apple" # True — membership test
3 not in [1, 2] # True
is to compare identity (e.g. x is None), and == to compare values.A carton holds 5 eggs. Replace each 0 with an expression so the program reports full cartons, leftovers, a power of two, and whether 'z' appears in 'pizza'.
eggs = 17
per_box = 5
print(f"Full boxes: {0}") # TODO: use //
print(f"Left over: {0}") # TODO: use %
print(f"2 ** 10 = {0}") # TODO: use **
print(f"Is 'z' in 'pizza'? {0}") # TODO: use in
eggs = 17
per_box = 5
print(f"Full boxes: {eggs // per_box}")
print(f"Left over: {eggs % per_box}")
print(f"2 ** 10 = {2 ** 10}")
print(f"Is 'z' in 'pizza'? {'z' in 'pizza'}")
Full boxes: 3 Left over: 2 2 ** 10 = 1024 Is 'z' in 'pizza'? True
Explain the difference between /, //, and % using pizza slices shared among friends.
You have 17 slices and 5 friends. 17 / 5 is the exact share, 3.4 slices each, if you're willing to cut slivers. 17 // 5 is how many whole slices each friend gets, 3. 17 % 5 is how many slices are left in the box afterwards, 2.
A string (str) is a sequence of characters used to represent text. Python accepts single quotes or double quotes interchangeably, so 'hi' and "hi" are the same, and triple quotes """…""" allow text that spans several lines. Because a string is a sequence, you can get its length with len(), read a single character by index (s[0] is the first; s[-1] is the last), and loop over its characters with for.
Slicing extracts part of a string with s[start:stop]. The slice includes start but stops before stop, so "hello"[1:4] is "ell". Either end may be left out, and an optional third number is a step, which is why s[::-1] reverses a string. Python strings are immutable: methods like upper(), strip(), and replace() never alter the original but return a new string, so you must use or store the result.
The modern way to build text from values is the f-string. Put an f before the opening quote and write any expression in braces: f"Hello, {name}!". f-strings can also format numbers, as in f"{price:.2f}" for two decimal places. Two methods do a lot of everyday work: split() breaks a string into a list of pieces, and "sep".join(list) glues a list of strings back together.
s[start:stop:step], a new string cut from part of another.f whose {…} parts are evaluated and inserted.first = "Alice"
last = "Smith"
# Concatenation
first + " " + last # "Alice Smith"
# f-strings (Python 3.6+) — preferred
f"Hello, {first}!" # "Hello, Alice!"
f"Age next year: {age + 1}" # expressions work too
# Common string methods
len("hello") # 5
"hello".upper() # "HELLO"
"HELLO".lower() # "hello"
" hello ".strip() # "hello"
"hello".replace("l", "r") # "herro"
"hello".startswith("he") # True
"hello".find("l") # 2
"a,b,c".split(",") # ["a","b","c"]
"-".join(["a", "b"]) # "a-b"
"hello"[1:4] # "ell" — slicing
"hello"[::-1] # "olleh" — reversed
Clean up the messy name so it prints as “Ada Lovelace” with its length, then build the initials “A.L.” from it. Two string methods cover the first line; split and join cover the second.
raw = " ada lovelace "
full = raw # TODO: strip the spaces and capitalise each word
initials = "" # TODO: first letter of each word, joined with "." plus a final "."
print(f"{full} ({len(full)} chars)")
print(f"Initials: {initials}")
raw = " ada lovelace "
full = raw.strip().title()
initials = ".".join(word[0] for word in full.split()) + "."
print(f"{full} ({len(full)} chars)")
print(f"Initials: {initials}")
Ada Lovelace (12 chars) Initials: A.L.
In "hello"[1:4], why do you get "ell" and not "ello"? Explain it with a ruler.
Picture the index numbers as marks on a ruler between the letters: mark 0 before the h, mark 1 before the first e, and so on. A slice cuts at two marks. Cutting at 1 and at 4 gives you whatever sits between those marks: e, l, l. The o is after mark 4, so it isn't included. A handy result: the length of a slice is simply stop - start.
A list is an ordered collection of values, written in square brackets: ["apple", "banana"]. It is Python's everyday, general-purpose container. Lists keep items in the order you put them, allow duplicates, and can hold values of any type, even a mix, though in practice a list usually holds one kind of thing.
Lists are mutable: unlike strings, they can be changed in place. append() adds to the end, insert() adds at a position, remove() deletes by value, pop() removes and returns an item, and sort() reorders the list itself. Lists use the same indexing and slicing as strings, so scores[-1] is the last item and scores[-2:] the last two. One consequence of mutability surprises beginners: b = a does not copy a list; it makes a second name for the same list, so changing one changes both. Use a.copy() for a real copy.
A list comprehension builds a new list from an existing sequence in a single, readable line: [expression for item in sequence if condition]. It replaces the common pattern of creating an empty list and appending to it in a loop. A tuple, written with parentheses, is like a list that cannot be changed. Use one for a fixed group of values, such as a coordinate (x, y), and unpack it into separate names with x, y = point.
[f(x) for x in items if cond], building a list in one expression.a, b = pair.fruits = ["apple", "banana", "cherry"]
fruits[0] # "apple"
fruits[-1] # "cherry" (last item)
fruits[1:3] # ["banana","cherry"] — slice
len(fruits) # 3
fruits.append("date") # add to end
fruits.insert(1, "avocado") # insert at index
fruits.remove("banana") # remove by value
fruits.pop() # remove & return last
fruits.sort() # sort in place
fruits.reverse() # reverse in place
"apple" in fruits # True
# [expression for item in iterable if condition]
squares = [x**2 for x in range(5)]
# [0, 1, 4, 9, 16]
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]
point = (10, 20) # immutable — cannot be changed
x, y = point # unpacking
Add 95 and sort the list, print the top two with a slice, then use a list comprehension to add 5 to every score without letting any go over 100.
scores = [88, 92, 75]
# TODO: add 95, then sort the list
print(f"Scores: {scores}")
print(f"Top two: {scores}") # TODO: slice out the last two
curved = [] # TODO: a list comprehension; min(s + 5, 100) caps each score
print(f"Curved: {curved}")
scores = [88, 92, 75]
scores.append(95)
scores.sort()
print(f"Scores: {scores}")
print(f"Top two: {scores[-2:]}")
curved = [min(s + 5, 100) for s in scores]
print(f"Curved: {curved}")
Scores: [75, 88, 92, 95] Top two: [92, 95] Curved: [80, 93, 97, 100]
Why does b = a followed by b.append(4) also change a? Use a shared shopping list as your example.
b = a doesn't photocopy the list; it just gives the same sheet of paper a second name. So when someone writes “4” on the sheet called b, anyone looking at the sheet called a sees it too, because there's only one sheet. a.copy() makes a real photocopy that can change independently.
A dictionary (dict) stores key–value pairs. Instead of finding a value by its numeric position, as with a list, you find it by a meaningful key: person["name"]. Keys must be unique and hashable, which in practice means immutable values such as strings, numbers, or tuples. Lookups are extremely fast no matter how large the dictionary grows, and since Python 3.7 dictionaries remember the order in which keys were inserted.
Reading a key that does not exist raises a KeyError. When a key might be missing, use get(key, default), which returns the default instead. Assigning to a key adds it or overwrites it, and del removes it. To walk through a dictionary, loop over items() to get each key and value together. A very common pattern is counting: counts[w] = counts.get(w, 0) + 1.
A set is an unordered collection of unique values, written with braces: {"red", "green"}. Adding an item that is already present does nothing, so converting a list to a set is the quickest way to remove duplicates. Sets also answer “is this in here?” very quickly and support the operations from mathematics: union |, intersection &, and difference -. Note that {} on its own creates an empty dictionary; an empty set is written set().
person = {
"name": "Alice",
"age": 30,
"city": "NYC",
}
person["name"] # "Alice"
person.get("email", "n/a") # "n/a" — safe access with default
person["email"] = "a@b.com" # add / update
del person["city"] # remove key
person.keys() # dict_keys(["name","age","email"])
person.values() # dict_values([...])
person.items() # dict_items([("name","Alice"),…])
"name" in person # True
# Dict comprehension
squares = {x: x**2 for x in range(5)}
# {0:0, 1:1, 2:4, 3:9, 4:16}
colors = {"red", "green", "blue"} # unique values only
colors.add("yellow")
colors.remove("red")
"green" in colors # True
a = {1, 2, 3}; b = {2, 3, 4}
a | b # {1,2,3,4} — union
a & b # {2,3} — intersection
a - b # {1} — difference
Count how many times each word appears using a dictionary and get, then use a set to list the distinct words in alphabetical order.
words = "the cat and the hat".split()
counts = {}
for word in words:
pass # TODO: add one to this word's count (start from 0)
print(counts)
unique = words # TODO: make a set of the words
print(f"Unique: {sorted(unique)}")
words = "the cat and the hat".split()
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
unique = set(words)
print(f"Unique: {sorted(unique)}")
{'the': 2, 'cat': 1, 'and': 1, 'hat': 1}
Unique: ['and', 'cat', 'hat', 'the']
When should you reach for a dictionary instead of a list? Give an everyday example of each.
A list is like a queue: what matters is the order, and you find things by their position. A dictionary is like a coat check: you hand over a ticket (the key) and get exactly your coat (the value) back straight away, without searching through every coat. If you ever find yourself searching a list for the item with a certain name, a dictionary keyed by that name is the better tool.
By default Python runs statements in order, top to bottom. Control structures let a program choose what to do, running some code only when a condition is true. This is how a program responds differently to different data.
The if statement is followed by a condition and a colon. The indented lines beneath it form its block, which runs only when the condition is truthy. elif (short for “else if”) tests further conditions in turn, and else catches everything left over. Python checks the branches from the top and runs only the first one that matches, so order them from most specific to least. For a quick two-way choice inside an expression, Python has the conditional expression: "adult" if age >= 18 else "minor".
Python 3.10 added structural pattern matching with match and case. The value after match is compared against each case pattern in turn, and the first that fits runs; case _ is the wildcard that matches anything. At its simplest it replaces a long if/elif chain of equality tests, but patterns can also pull apart lists, tuples, and dictionaries, which makes match powerful for handling structured data such as commands or messages.
if, loop, function, or class.match/case, choosing a branch by the shape and value of data.score = 75
if score >= 90:
print("A")
elif score >= 75:
print("B")
elif score >= 60:
print("C")
else:
print("F")
status = "adult" if age >= 18 else "minor"
match command:
case "quit":
print("Quitting")
case "help":
print("Showing help")
case _:
print("Unknown command")
Finish grade with if/elif/else (90+ A, 80+ B, 70+ C, 60+ D, otherwise F), set parity with a conditional expression, and add the match cases.
def grade(score):
# TODO: if / elif / else returning "A", "B", "C", "D" or "F"
pass
print(grade(95), grade(82), grade(64), grade(40))
n = 7
parity = "" # TODO: "even" or "odd" using ... if ... else ...
print(f"{n} is {parity}")
command = "help"
match command:
# TODO: case "quit" -> "Quitting", case "help" -> "Showing help",
# anything else -> "Unknown command"
case _:
pass
def grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
print(grade(95), grade(82), grade(64), grade(40))
n = 7
parity = "even" if n % 2 == 0 else "odd"
print(f"{n} is {parity}")
command = "help"
match command:
case "quit":
print("Quitting")
case "help":
print("Showing help")
case _:
print("Unknown command")
A B D F 7 is odd Showing help
If the first test in grade were score >= 60, what would a score of 95 get, and why?
It would get a D. Python walks down the chain and stops at the first test that passes, and 95 is certainly at least 60. It never gets as far as the A test. The fix is to put the strictest test first, so a score falls through to lower grades only after failing the higher ones.
A loop repeats a block of code. Repetition is at the core of programming: processing each line of a file, each item in a list, or each row of a spreadsheet. Each pass through the loop's body is called an iteration.
Python's for loop is different from the counting loop found in C-family languages: it walks through the items of any iterable, such as a list, string, dictionary, or file, handing you one item per iteration. When you do want numbers, range() produces them: range(5) gives 0 to 4, and range(2, 10, 2) counts from 2 up to (but not including) 10 in steps of 2. Two helpers keep loops tidy: enumerate() gives you a running index alongside each item, and zip() walks two sequences side by side.
The while loop repeats as long as its condition remains true. It is the right choice when you don't know in advance how many iterations you need, such as reading input until the user types “quit”. Inside any loop, break exits immediately, and continue skips to the next iteration. A while loop whose condition never becomes false runs forever, a common bug when the loop variable is never updated.
for loop can walk through: lists, strings, dicts, files, ranges.for i, x in enumerate(items).# Iterate over a list
for fruit in ["apple", "banana"]:
print(fruit)
# range() — generate a sequence of numbers
for i in range(5): # 0 1 2 3 4
print(i)
for i in range(2, 10, 2): # 2 4 6 8 (start, stop, step)
print(i)
# enumerate() — get index AND value
for i, fruit in enumerate(["apple", "banana"]):
print(i, fruit) # 0 apple, 1 banana
# zip() — iterate over two lists together
for name, score in zip(["Alice", "Bob"], [90, 85]):
print(f"{name}: {score}")
i = 0
while i < 5:
print(i)
i += 1
break to exit a loop early, continue to skip to the next iteration, and pass as a no-op placeholder.Write FizzBuzz for 1 to 15 (“Fizz” for multiples of 3, “Buzz” for 5, “FizzBuzz” for both), then number the fruits starting from 1 using enumerate.
parts = []
# TODO: loop i over 1..15 with range() and append the right word (or str(i))
print(" ".join(parts))
# TODO: print "1. apple" and "2. banana" using enumerate(..., start=1)
parts = []
for i in range(1, 16):
if i % 15 == 0:
parts.append("FizzBuzz")
elif i % 3 == 0:
parts.append("Fizz")
elif i % 5 == 0:
parts.append("Buzz")
else:
parts.append(str(i))
print(" ".join(parts))
for number, fruit in enumerate(["apple", "banana"], start=1):
print(f"{number}. {fruit}")
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 1. apple 2. banana
Why does range(1, 16) stop at 15 instead of 16? Explain why that's actually convenient.
The stop number is a fence you don't cross, just like slice ends. That way range(n) gives exactly n numbers, 0 through n-1, which are exactly the valid indexes of a list of length n. And range(a, b) always has b - a numbers, so you rarely have to fiddle with plus or minus one.
A function is a named, reusable block of code that performs a single task. You define one with def, give it a name and a list of parameters in parentheses, and indent its body underneath. Calling the function, as in greet("Ada"), runs the body with the parameters set to the arguments you supplied. Functions are the main way to break a large problem into small, testable, named pieces, and to avoid writing the same steps twice.
A function sends a value back with return. A function that reaches its end without a return gives back None. To return several values, return them separated by commas; Python packs them into a tuple, which the caller can unpack with lo, hi = min_max(values). Parameters may have default values (def greet(name="World")), which makes them optional, and arguments can be passed by name, as in greet(name="Ada"), which makes calls self-explanatory.
For a flexible number of inputs, *args collects any extra positional arguments into a tuple, and **kwargs collects extra named arguments into a dictionary. A lambda is a small anonymous function limited to a single expression, handy for passing a quick rule such as a sort key. Variables created inside a function are local: they exist only while the function runs and are invisible outside it. This scope rule keeps functions from accidentally interfering with each other.
None if there's no return.# Basic function
def greet(name):
return f"Hello, {name}!"
greet("Alice") # "Hello, Alice!"
# Default parameters
def greet(name="World"):
return f"Hello, {name}!"
greet() # "Hello, World!"
# *args — variable positional arguments
def total(*nums):
return sum(nums)
total(1, 2, 3, 4) # 10
# **kwargs — variable keyword arguments
def describe(**info):
for k, v in info.items():
print(f"{k}: {v}")
describe(name="Alice", age=30)
# Lambda — anonymous single-expression function
double = lambda x: x * 2
double(5) # 10
Write the bodies: total adds any number of arguments, min_max returns the smallest and largest as a pair, and greet uses a default parameter.
def total(*nums):
pass # TODO: add up nums and return the result
def min_max(values):
pass # TODO: return two values: the min and the max
def greet(name="World"):
pass # TODO: return "Hello, <name>!"
print(f"Sum: {total(1, 2, 3, 4, 5)}")
lo, hi = min_max([4, 1, 9, 3])
print(f"Min: {lo}, Max: {hi}")
print(greet())
def total(*nums):
result = 0
for n in nums:
result += n
return result
def min_max(values):
return min(values), max(values)
def greet(name="World"):
return f"Hello, {name}!"
print(f"Sum: {total(1, 2, 3, 4, 5)}")
lo, hi = min_max([4, 1, 9, 3])
print(f"Min: {lo}, Max: {hi}")
print(greet())
Sum: 15 Min: 1, Max: 9 Hello, World!
What's the difference between print-ing a result inside a function and return-ing it? Use a vending machine.
Printing is the machine flashing “here's your drink” on its screen: you see it, but you can't do anything with it. Returning is the drink actually dropping into the tray, so the caller can take it and use it, whether that means storing it, adding it to something, or passing it on. A function that only prints can't feed its result into the next step of your program.
A module is simply a Python file whose functions, classes, and variables can be used by other code. Splitting a program into modules keeps each file focused and lets you reuse code across projects. A folder of related modules is called a package. You bring a module's contents into your program with an import statement.
There are three common forms. import math loads the module and you refer to its contents with a dot, as in math.sqrt(16). The prefix makes it obvious where each name comes from. from math import sqrt brings a specific name directly into your file, so you can write sqrt(16). import datetime as dt gives a module a shorter alias. Avoid from module import *, which dumps every name into your file and makes it hard to tell where anything came from.
Python is famous for coming with “batteries included”: its standard library ships modules for maths (math, statistics), dates (datetime), files and folders (os, pathlib), data formats (json, csv), text patterns (re), and much more, all available without installing anything. Beyond that, hundreds of thousands of third-party packages are published on PyPI and installed with pip.
.py file whose contents can be imported by other code.import datetime as dt.# Import a whole module
import math
math.sqrt(16) # 4.0
math.pi # 3.14159…
# Import specific names
from math import sqrt, ceil
sqrt(25) # 5.0
# Import with alias
import datetime as dt
dt.date.today()
# Useful standard library modules
import os # file system, environment variables
import sys # interpreter info, argv
import json # parse/write JSON
import re # regular expressions
import random # random numbers
import datetime # dates and times
Using only the standard library, fill in the imports so these four lines work: the length of a 3-4 hypotenuse, an average, the weekday of 1 January 2026, and a dictionary turned into JSON text.
# TODO: import json and math; from datetime import date; from statistics import mean
print(math.hypot(3, 4))
print(mean([2, 4, 9]))
print(date(2026, 1, 1).strftime("%A"))
print(json.dumps({"name": "Ada", "age": 36}))
import json
import math
from datetime import date
from statistics import mean
print(math.hypot(3, 4))
print(mean([2, 4, 9]))
print(date(2026, 1, 1).strftime("%A"))
print(json.dumps({"name": "Ada", "age": 36}))
5.0
5
Thursday
{"name": "Ada", "age": 36}
Why might a team prefer import math and writing math.sqrt over from math import *? Explain with a library of books.
math.sqrt is like a citation: anyone reading the code can see exactly which book the idea came from. import * tips every page of the book onto your desk, mixed in with your own notes. Later nobody can tell which names are yours and which came from where, and if two books use the same word, one silently covers the other.
Variables vanish when a program ends. To keep data between runs, or to share it with other programs, you write it to a file. File I/O (input/output) means reading data from files and writing data to them. The built-in open() function connects your program to a file and returns a file object that you read from or write to.
The second argument to open() is the mode. "r" reads (the default) and fails if the file doesn't exist. "w" writes, creating the file or erasing it if it already exists. "a" appends to the end, keeping what's there. Text is read with read() (the whole file as one string), or, most usefully, by looping over the file object, which yields one line at a time. Each line includes its trailing newline character \n, which is why you'll often call strip() on it. write() does not add newlines for you.
Open files use operating-system resources and must be closed. The with statement handles that automatically: the file is closed when the indented block ends, even if an error occurs partway through. This pattern is called a context manager, and it is the standard way to work with files in Python. For structured data, the standard library's csv and json modules read and write common formats directly, and pathlib offers a modern, object-oriented way to handle file paths.
"r" read, "w" write (overwrites), "a" append.\n character that ends each line of a text file.with block that sets something up and always cleans it up afterwards.# Write to a file
with open("data.txt", "w") as f:
f.write("Hello, file!\n")
# Read entire file
with open("data.txt", "r") as f:
contents = f.read()
# Read line by line
with open("data.txt") as f:
for line in f:
print(line.strip())
# Append to a file
with open("data.txt", "a") as f:
f.write("New line\n")
with statement when working with files — it automatically closes the file even if an error occurs.Write three items to shopping.txt, append a fourth, then read the file back, printing each line numbered and the total count. The program creates the file in whatever folder you run it from.
# TODO: open shopping.txt in "w" mode and write eggs, milk and bread, one per line
# TODO: open it in "a" mode and write coffee
count = 0
# TODO: open it for reading; for each line print "<number>: <item>" and add to count
print(f"{count} items")
with open("shopping.txt", "w") as f:
f.write("eggs\nmilk\nbread\n")
with open("shopping.txt", "a") as f:
f.write("coffee\n")
count = 0
with open("shopping.txt") as f:
for number, line in enumerate(f, start=1):
print(f"{number}: {line.strip()}")
count += 1
print(f"{count} items")
1: eggs 2: milk 3: bread 4: coffee 4 items
Why is "w" mode dangerous in a way "a" isn't? Use a notebook as the example.
Opening in "w" is like tearing out every page of the notebook before you start writing. Whatever was there is gone the moment you open it, even if you then write nothing. "a" turns to the first blank page and adds after what's already there. Use "w" only when you really mean to replace the whole file.
Object-oriented programming (OOP) organises a program around objects: bundles of related data together with the functions that work on that data. A class, defined with the class keyword, is the blueprint, and an object (or instance) is a thing built from it by calling the class like a function: Dog("Rex"). In fact, you've used objects all along. Every string, list, and dictionary in Python is an object, and "hi".upper() is calling a method on one.
Functions defined inside a class are called methods. Their first parameter, conventionally named self, is the particular object the method is working on, and Python passes it automatically. The special method __init__ is the initialiser: it runs when an object is created and typically stores the object's starting data as attributes (self.name = name). Other dunder (“double underscore”) methods hook into Python's built-in behaviour. __str__, for instance, controls what print() shows for your object.
Inheritance lets a new class build on an existing one: class Dog(Animal) receives everything Animal has and can add to it or override methods by redefining them. super() calls the parent's version when you want to extend rather than replace it. The payoff is polymorphism: code that calls pet.speak() works for dogs, cats, or any animal subclass, and each object answers in its own way.
class Animal:
"""A simple Animal class."""
# Class variable (shared by all instances)
kingdom = "Animalia"
def __init__(self, name: str, age: int):
# Instance variables
self.name = name
self.age = age
def describe(self) -> str:
return f"{self.name} is {self.age} years old."
def __str__(self):
return self.name
# Inheritance
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # call parent __init__
self.breed = breed
def speak(self) -> str:
return f"{self.name} says: Woof!"
dog = Dog("Rex", 4, "Labrador")
print(dog.describe()) # Rex is 4 years old.
print(dog.speak()) # Rex says: Woof!
print(Dog.kingdom) # Animalia
| Method | Purpose |
|---|---|
__init__ | Constructor — called when creating an instance |
__str__ | String representation — used by print() |
__repr__ | Developer representation — used in the REPL |
__len__ | Called by len() |
__eq__ | Called by == |
Animal is written. Add Dog and Cat subclasses that override speak; the loop then works for both without changes.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
def __str__(self):
return f"Animal: {self.name}"
# TODO: class Dog(Animal) whose speak() returns "<name> says Woof!"
# TODO: class Cat(Animal) whose speak() returns "<name> says Meow!"
pets = [Dog("Rex"), Cat("Tom")]
for pet in pets:
print(pet.speak())
print(pets[0])
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
def __str__(self):
return f"Animal: {self.name}"
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
pets = [Dog("Rex"), Cat("Tom")]
for pet in pets:
print(pet.speak())
print(pets[0])
Rex says Woof! Tom says Meow! Animal: Rex
What is self, and why does every method need it? Explain it with name tags at a party.
A class is one set of instructions shared by every object, like party rules everyone follows. When Rex is told “introduce yourself”, the instruction needs to know whose name tag to read. self is “the guest being spoken to right now”. self.name reads that guest's own tag, so the same instructions give “Rex” for Rex and “Tom” for Tom.