Back to Python Examples

Basic Python

A quick-reference and hands-on guide to the fundamentals of Python programming.

How to use this page

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:

  1. Read the lesson once.
  2. Explain it out loud in plain words, as if teaching a friend — no jargon. If you stall, that's the gap.
  3. Do the exercise from a blank editor, without peeking. Where you get stuck is exactly what to reread.
  4. Reveal the solution, compare, and explain the idea again in your own words.

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.

Contents

  1. What is Python?
  2. Variables & Data Types
  3. Operators
  4. Strings
  5. Lists
  6. Dictionaries & Sets
  7. Control Structures
  8. Loops
  9. Functions
  10. Modules & Imports
  11. File I/O
  12. Intro to OOP

1. What is Python?

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.

Interpreter
The program that reads Python source and executes it, run as python3 file.py.
REPL
Read–Eval–Print Loop: the interactive >>> prompt for trying code line by line.
Dynamic typing
Types are attached to values and checked at run time, not declared on variables.
# Single-line comment

"""
Multi-line string / docstring
Often used as a comment block
"""

print("Hello, World!")
Explain it simply

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.

Reveal a plain-language answer

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.

2. Variables & Data Types

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.

Assignment
Binding a name to a value with =, which creates the variable if it doesn't exist.
Type conversion
Making a value of one type from another, such as int("42").
Truthiness
How a value behaves in a condition: empty and zero values are false.
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
TypeExampleDescription
str"hello"Text
int42Whole numbers
float3.14Decimal numbers
boolTrue / FalseBoolean values
NoneNoneAbsence 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
Exercise — your turn

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
Show solution
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)
Output
Ada is 36 years old and 1.65 m tall.
Admin: False
<class 'float'>
43
Explain it simply

Why does "4" + 2 fail in Python while int("4") + 2 works? Explain it without the word “type”.

Reveal a plain-language answer

"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.

3. Operators

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.

Floor division
//, division rounded down to a whole number: 17 // 5 is 3.
Membership
in / not in, testing whether a value is inside a collection.
Identity vs. equality
is asks “same object?”; == asks “same value?”

Arithmetic

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

Comparison

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

Logical

a and b   # True if both are truthy
a or  b   # True if either is truthy
not a      # inverts the boolean

Identity & Membership

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
Use is to compare identity (e.g. x is None), and == to compare values.
Exercise — your turn

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
Show solution
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'}")
Output
Full boxes: 3
Left over: 2
2 ** 10 = 1024
Is 'z' in 'pizza'? True
Explain it simply

Explain the difference between /, //, and % using pizza slices shared among friends.

Reveal a plain-language answer

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.

4. Strings

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.

Index
A character's position, counting from 0; negative indexes count from the end.
Slice
s[start:stop:step], a new string cut from part of another.
f-string
A string prefixed with 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
Exercise — your turn

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}")
Show solution
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}")
Output
Ada Lovelace (12 chars)
Initials: A.L.
Explain it simply

In "hello"[1:4], why do you get "ell" and not "ello"? Explain it with a ruler.

Reveal a plain-language answer

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.

5. Lists

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.

Mutable
Changeable in place; lists are mutable, strings and tuples are not.
List comprehension
[f(x) for x in items if cond], building a list in one expression.
Unpacking
Assigning the items of a sequence to several names at once: 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

List Comprehensions

# [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]

Tuples

point = (10, 20)       # immutable — cannot be changed
x, y = point           # unpacking
Exercise — your turn

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}")
Show solution
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}")
Output
Scores: [75, 88, 92, 95]
Top two: [92, 95]
Curved: [80, 93, 97, 100]
Explain it simply

Why does b = a followed by b.append(4) also change a? Use a shared shopping list as your example.

Reveal a plain-language answer

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.

6. Dictionaries & Sets

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().

Key–value pair
A dictionary entry: a unique key mapped to one value.
Hashable
Usable as a dict key or set member; in practice, immutable.
KeyError
The error raised when you read a dictionary key that doesn't exist.

Dictionaries

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}

Sets

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
Exercise — your turn

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)}")
Show solution
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)}")
Output
{'the': 2, 'cat': 1, 'and': 1, 'hat': 1}
Unique: ['and', 'cat', 'hat', 'the']
Explain it simply

When should you reach for a dictionary instead of a list? Give an everyday example of each.

Reveal a plain-language answer

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.

7. Control Structures

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.

Condition
An expression evaluated for truthiness to decide which branch runs.
Block
The indented lines that belong to an if, loop, function, or class.
Pattern matching
match/case, choosing a branch by the shape and value of data.

if / elif / else

score = 75

if score >= 90:
    print("A")
elif score >= 75:
    print("B")
elif score >= 60:
    print("C")
else:
    print("F")

Ternary (Conditional Expression)

status = "adult" if age >= 18 else "minor"

match / case (Python 3.10+)

match command:
    case "quit":
        print("Quitting")
    case "help":
        print("Showing help")
    case _:
        print("Unknown command")
Exercise — your turn

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
Show solution
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")
Output
A B D F
7 is odd
Showing help
Explain it simply

If the first test in grade were score >= 60, what would a score of 95 get, and why?

Reveal a plain-language answer

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.

8. Loops

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.

Iterable
Anything a for loop can walk through: lists, strings, dicts, files, ranges.
range()
Generates a sequence of integers; the stop value is excluded.
enumerate()
Pairs each item with its position: for i, x in enumerate(items).

for

# 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}")

while

i = 0
while i < 5:
    print(i)
    i += 1
Use break to exit a loop early, continue to skip to the next iteration, and pass as a no-op placeholder.
Exercise — your turn

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)
Show solution
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}")
Output
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
1. apple
2. banana
Explain it simply

Why does range(1, 16) stop at 15 instead of 16? Explain why that's actually convenient.

Reveal a plain-language answer

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.

9. Functions

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.

Parameter / argument
The name in the definition / the actual value passed in the call.
Return value
The result a function hands back; None if there's no return.
Scope
Where a name is visible; names created inside a function are local to it.
# 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
Exercise — your turn

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())
Show solution
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())
Output
Sum: 15
Min: 1, Max: 9
Hello, World!
Explain it simply

What's the difference between print-ing a result inside a function and return-ing it? Use a vending machine.

Reveal a plain-language answer

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.

10. Modules & Imports

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.

Module
A .py file whose contents can be imported by other code.
Standard library
The modules that come with Python itself, no installation needed.
Alias
A shorter name given at import time: 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
Exercise — your turn

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}))
Show solution
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}))
Output
5.0
5
Thursday
{"name": "Ada", "age": 36}
Explain it simply

Why might a team prefer import math and writing math.sqrt over from math import *? Explain with a library of books.

Reveal a plain-language answer

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.

11. File I/O

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.

Mode
How a file is opened: "r" read, "w" write (overwrites), "a" append.
Newline
The \n character that ends each line of a text file.
Context manager
A 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")
Always use the with statement when working with files — it automatically closes the file even if an error occurs.
Exercise — your turn

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")
Show solution
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")
Output
1: eggs
2: milk
3: bread
4: coffee
4 items
Explain it simply

Why is "w" mode dangerous in a way "a" isn't? Use a notebook as the example.

Reveal a plain-language answer

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.

12. Intro to OOP

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 / instance
The blueprint / an object created from it.
self
The first parameter of every method: the object the method was called on.
Override
Redefining an inherited method in a subclass to change its behaviour.
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
MethodPurpose
__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 ==
Exercise — your turn

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])
Show solution
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])
Output
Rex says Woof!
Tom says Meow!
Animal: Rex
Explain it simply

What is self, and why does every method need it? Explain it with name tags at a party.

Reveal a plain-language answer

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.