The programs you have written so far fit on one screen. You could read them from top to bottom and hold every line in your head. The apps on your phone are not like that. A mobile wallet, a bus-booking site or the program a school uses for its results is thousands of lines long, often hundreds of thousands, written by many people over many years, and built on code written by people its programmers have never met. Nobody holds all of it in their head. Yet these programs work, and they can be changed without breaking.
They stay in order because they are built from parts, each with one job and a clear way to use it. In Grade 8 you met the smallest part: the function. In this unit you go up from there. You will build a program from functions and read other people’s functions closely, learn exactly which variables a function can see, split a program into files called modules, use libraries of code that others wrote by reading their documentation, and get data from another computer through an API. By the end you will have written a program the way programmers really work.
A program built from functions
Ms. Karki, a class teacher in Bhaktapur, keeps her class’s marks from the terminal exam. Each subject’s theory paper is out of 75, as in the SEE. She wants a program that works out each student’s percentage and grade in every subject, says who has passed every subject (35% or more in each), and prints the results.
That is too big to write in one go, so begin with decomposition, as you did in Grade 8: break the job into parts, each small enough to write as one function. Then write the program’s main steps first, as if the functions already existed. This is called top-down design: start from the whole job at the top, then fill in the parts below it.
def main(): students = read_marks() # get the data for student in students: print_report(student) # one line per studentRead it aloud: get the marks, then print a report for each student. It reads like the plan, because the details are hidden inside functions with good names. Now each function can be written, and tested, one at a time.
Here is the whole program. The marks are typed into it for now, as a list of dictionaries (Grade 9): one dictionary for each student. Press Run.
PASS_MARK = 35 # percent, in every subjectGRADES = [(90, "A+"), (80, "A"), (70, "B+"), (60, "B"), (50, "C+"), (40, "C"), (35, "D")] def read_marks(): """The class's marks out of 75, one dictionary per student.""" return [ {"name": "Mina", "science": 63, "maths": 54, "computer": 70}, {"name": "Bikash", "science": 21, "maths": 40, "computer": 52}, {"name": "Sita", "science": 72, "maths": 69, "computer": 74}, {"name": "Pemba", "science": 45, "maths": 25, "computer": 60}, ] def percent(got, out_of): """got marks out of out_of, as a whole-number percentage.""" return round(got * 100 / out_of) def grade_for(p): """The letter grade for a percentage p, from A+ down to NG.""" for lowest, grade in GRADES: if p >= lowest: return grade return "NG" def print_report(student): """Print one student's grades on one line, and whether they passed.""" line = student["name"] + ":" passed = True for subject in ["science", "maths", "computer"]: p = percent(student[subject], 75) line = line + " " + subject + " " + grade_for(p) if p < PASS_MARK: passed = False if passed: line = line + " (passed)" print(line) def main(): students = read_marks() for student in students: print_report(student) main()Output
Mina: science A maths B+ computer A+ (passed)Bikash: science NG maths C+ computer BSita: science A+ maths A+ computer A+ (passed)Pemba: science B maths NG computer ASome things to notice:
- The program’s last line,
main(), is the only line outside the functions that does any work. Everything starts there. Putting a program’s steps in a function called main is a habit, not a rule of Python, but most programmers follow it, because then every line belongs to a function with a name. GRADESis a list of tuples (Grade 9): each pair is the lowest percentage for a grade, and the grade.for lowest, grade in GRADES:takes each pair apart into two variables. To change the grading table, you change only this list.- PASS_MARK and GRADES are written in capitals. Python doesn’t care, but programmers use capitals for a constant: a value set once, at the top, and never changed while the program runs.
- Each function has a docstring (Grade 8) that says in one sentence what it does.
Functions that answer, and functions that do
Look again at the functions, and sort them into two kinds.
- percent and grade_for only answer. They take values in through their parameters, work something out, and give it back with
return. They print nothing and change nothing else. Callgrade_for(84)a thousand times and it gives “A” a thousand times. A function like this is called a pure function. - print_report does something: it prints. read_marks could read a file. Anything a function does besides giving back a value (printing, changing a variable outside itself, writing a file, sending a message) is called a side effect.
Programs need side effects: a program that printed nothing and saved nothing would be useless. But pure functions are the easiest to trust. You can test one by calling it with a few values and checking the answers, without setting up anything else, and you can use it in any other program. So good programmers keep most of the work in pure functions, and the side effects in a few functions at the edges: the ones that read the input and the ones that show the results.
Which of these is a pure function?
Pick an answer to see why.
Reading a function
You will spend more of your life reading code than writing it: other people’s code, and your own from last year, which by then feels like someone else’s. Reading a function well is a skill, and it follows the same few questions every time:
- Its name and docstring. What does it say it does?
- What goes in. What are its parameters, and what kind of value must each be: a number, a string, a list, a dictionary? If there is no docstring, the way each parameter is used tells you (
student["name"]means student is a dictionary with a key “name”). - What it builds. Which variables and data structures does it make inside: a counter, a list it adds to, a dictionary it fills?
- What comes out. What does
returngive back, and of what kind? Is there a path through the function with noreturn, so that it gives backNone? - What else it does. Does it print, change a list it was given, or change anything outside itself?
- Try it by hand. Pick small inputs and trace it, line by line, as in Grade 8.
Then say what it does in one sentence, in your own words.
Giving back more than one value
A function can give back several values at once, by returning them with commas between them. Python packs them into a tuple, and the caller can unpack them into separate variables:
def lowest_and_highest(marks): """The smallest and the largest number in a list of marks.""" return min(marks), max(marks) low, high = lowest_and_highest([63, 21, 72, 45])print("Lowest:", low, " Highest:", high)Output
Lowest: 21 Highest: 72A list you pass in can be changed
There is one thing about parameters that surprises almost everyone. When you pass a list (or a dictionary) to a function, the function doesn’t get a copy. Its parameter is a second name for the same list. So if the function changes the list, the caller’s list changes too.
def top_three(marks): """The three highest marks, highest first.""" marks.sort(reverse=True) return marks[:3] science = [63, 21, 72, 45]print(top_three(science))print(science)Output
[72, 63, 45][72, 63, 45, 21]The function answered correctly, but look at the second line: the class’s own list of science marks is now in a different order. marks.sort() sorts the list it is given, in place, and marks and science are two names for one list. If the order mattered (if the marks were in the same order as the names in another list), the program now gives each student someone else’s mark, and nothing warns you.
def add_bonus(marks): for i in range(len(marks)): marks[i] = marks[i] + 5. A program runs science = [63, 21, 72], then add_bonus(science), then print(science). What does it print?
Pick an answer to see why.
Scope in full
In Grade 8 you saw that a function’s variables are its own: a variable made inside a function can’t be seen outside it. This section gives the full rules: which names each line of a program can see. The part of a program where a name can be used is called the name’s scope.
Every call gets its own box
Each time a function is called, Python makes it a fresh box for its own variables: its parameters, and every variable it assigns. Variables in that box are called local variables. While the call runs, its box sits on top of the boxes of whoever called it. When the call returns, its box is thrown away, with everything in it. The program itself has one box that lasts as long as the program: its global variables, the names made outside every function (and the names of its functions, which def makes).
Step through this program and watch the boxes. Both functions have a variable called p, and both have a parameter called got.
Three things to see:
- The two variables called got, and the two called p, never touch each other. Each lives in its own call’s box.
- Bikash’s call starts with a brand-new box. Nothing is left over from Mina’s call.
- The boxes pile up, the newest on top: the program calls result, which calls percent. When percent returns, its box goes and result carries on with its own. A pile like this, where the last one on is the first one off, is called a stack, and this one is the call stack.
Where Python looks for a name
When a line uses a name, Python searches the boxes in a fixed order, and uses the first one it finds:
- Local: the box of the function that is running now.
- Global: the program’s own box.
- Built-in: the names Python always has:
print,len,round,sum,int,strand about 140 more.
That is why PASS_MARK works inside result: it isn’t in result’s own box, so Python goes on to the global box and finds it there. And round works anywhere, because it is a built-in.
Notice what is not on the list: the box of the function that called this one. percent is called by result, but it can’t see result’s name. A function sees only its own names, the program’s globals and the built-ins. Everything else it needs must come in through its parameters.
Same name, different variable
What if a function assigns to a name that is also a global? Step through this one and predict: what does the last line print?
count = 1 inside the function doesn’t change the global count. Assigning to a name inside a function always makes (or changes) a local variable of that function. So now there are two variables called count, one in each box, and inside the function the local one hides the global one. A local variable that hides a global of the same name is said to shadow it.
This rule is what makes functions safe to write. You can name a variable total or count in your function without checking every other name in a program of ten thousand lines.
Changing a global from inside a function
The same rule catches people who try to keep a running total in a global:
total = 0 def add(marks): total = total + marks add(45)print(total)Error message
Traceback (most recent call last): File "main.py", line 6, in <module> add(45) ~~~^^^^ File "main.py", line 4, in add total = total + marks ^^^^^UnboundLocalError: cannot access local variable 'total' where it is not associated with a valuePython decides which names are local before the function runs, by looking at the whole function: add assigns to total, so total is local everywhere in add. Then line 4 tries to read that local total before anything has been put in it. That is an UnboundLocalError.
Python does have a way to say “I mean the global one”: a line global total at the top of the function.
total = 0 def add(marks): global total total = total + marks add(45)add(30)print(total)Output
75It works, but programmers avoid it. Once any function can change a global, then when the global has a wrong value, the bug could be in any function of the program, and you have to read them all to find it. And a function that reaches outside its box can’t be tested, or reused in another program, on its own. The better way is the one you know: values go in through parameters, and come out through return.
def add(total, marks): """The new total after adding marks to it.""" return total + marks total = 0total = add(total, 45)total = add(total, 30)print(total)Output
75Now add is a pure function, and every change to total happens in the program’s own lines, where you can see it.
A program has rate = 13 at the top, outside every function. A function contains only return price * rate / 100. What happens when it is called?
Pick an answer to see why.
Modules: a program in several files
Ms. Karki’s program will grow: reading the marks from a file, more subjects, a table for the notice board, results saved for next year. A single file of a few thousand lines is hard to find your way around, and two people can’t easily work on it at once. So programs are split into several files.
A module is a file of Python code (a .py file) whose functions and variables other programs can use. You have used modules since Grade 8: turtle, random, math and time are modules that come with Python. Any .py file you write is a module too.
Your first module
Here are the school’s grading rules, moved out of the program into a file of their own, grading.py. Most of it is the program’s old functions; the last part is new, and explained below. This page keeps the file beside the programs that use it, as a computer keeps files in a folder.
grading.py
"""The school's grading rules: percentages, grades and passing.""" PASS_MARK = 35 # percent, in every subjectGRADES = [(90, "A+"), (80, "A"), (70, "B+"), (60, "B"), (50, "C+"), (40, "C"), (35, "D")] def percent(got, out_of): """got marks out of out_of, as a whole-number percentage.""" return round(got * 100 / out_of) def grade_for(p): """The letter grade for a percentage p, from A+ down to NG.""" for lowest, grade in GRADES: if p >= lowest: return grade return "NG" def passed_all(percents): """True if every percentage in the list is PASS_MARK or more.""" return min(percents) >= PASS_MARK if __name__ == "__main__": # tests: these run only when grading.py itself is run print(grade_for(90), grade_for(89), grade_for(35), grade_for(34)) print(passed_all([35, 90]), passed_all([34, 90]))Any program in the same folder can now use it. import grading runs the file once and makes a name, grading, for everything in it. Its contents are reached with a dot:
import grading p = grading.percent(54, 75)print(p, grading.grade_for(p))print("Pass mark:", grading.PASS_MARK)Output
72 B+Pass mark: 35Three ways to import
import grading # 1. use as grading.grade_for(72)from grading import grade_for, percent # 2. use as grade_for(72)import grading as gr # 3. use as gr.grade_for(72)import gradingkeeps everything behind the module’s name. It is longer to type, but every line says where its name came from:grading.percentis clearly the school’s rule.from grading import grade_for, percentcopies just those names into your program’s global box, so you can use them without the dot. Handy for a few names you use often.import … as …gives the module a shorter name. Some libraries have names everyone shortens the same way, as you will see in Unit 4.
There is a fourth way, from grading import *, which copies every name the module has into your program. Avoid it: you can’t see which names it brought in, and any of them can shadow one of yours without a word.
Each module has its own globals
A module is a box of names too. PASS_MARK in grading.py is a global of grading.py, not of your program: your program reaches it as grading.PASS_MARK. If your program also had a variable called PASS_MARK, the two would be different variables, one in each box. A box of names like this is called a namespace. Because every module has its own namespace, two modules written by different people can both have a function called read or percent without clashing: grading.percent and maths_tools.percent are different names.
Testing a module by itself
Look at the end of grading.py again: if __name__ == "__main__":. Every module has a variable __name__ (two underscores on each side), which Python sets before the file runs:
- when the file is the program you run,
__name__is"__main__"; - when the file is imported,
__name__is the module’s name, here"grading".
So the lines under that if run only when grading.py is run by itself, and not when another program imports it. That is the place for a module’s own tests. On a computer you would run grading.py and check that its tests print what you expect:
Output
A+ A D NGTrue False90% is an A+ and 89% only an A; 35% is just a D and 34% is NG: the edges of the table are where mistakes hide, so they are what the tests check. When the program imported grading.py above, none of this was printed.
Splitting a program into modules
How do you decide what goes in which module? The same way you decide what goes in which function, one level up: each module looks after one area of the program, and its functions belong together. Ms. Karki’s program splits naturally into three: reading and saving the class’s files, the school’s rules, and showing the results.
Two signs of a good split:
- Each module has one job. Everything about the marks file is in one module. If the school starts keeping its marks in a spreadsheet, only that module changes.
- Modules need little from each other. The report module uses the rules, but only by calling a few functions such as
grade_for(). It doesn’t need to know how they work inside.
Here is the rest of the program. The marks now come from a file, marks.csv, as in Grade 9:
marks.csv
name,science,maths,computerMina Shrestha,63,54,70Bikash Tamang,21,40,52Sita Yadav,72,69,74Pemba Sherpa,45,25,60marks_file.py
"""Reading the class's marks from a CSV file.""" SUBJECTS = ["science", "maths", "computer"] def read_marks(file_name): """A list of students from a marks file: one dictionary per row, like {"name": "Mina Shrestha", "science": 63, ...}.""" file = open(file_name) lines = file.read().splitlines() file.close() header = lines[0].split(",") students = [] for line in lines[1:]: values = line.split(",") student = {"name": values[0]} for i in range(1, len(header)): student[header[i]] = int(values[i]) students.append(student) return studentsreport.py
"""Showing the class's results.""" import grading def report_line(student, subjects): """One student's grades as a line of text, with (passed) at the end if they passed every subject.""" percents = [grading.percent(student[s], 75) for s in subjects] grades = [s + " " + grading.grade_for(p) for s, p in zip(subjects, percents)] line = student["name"] + ": " + ", ".join(grades) if grading.passed_all(percents): line = line + " (passed)" return linereport.py uses two things that may be new. [grading.percent(student[s], 75) for s in subjects] is a list comprehension: a short way to build a list, read as “the percent of each subject s in subjects”. It does what a for loop with append() does, in one line. And zip(subjects, percents) walks two lists side by side, a pair at a time: (“science”, 84), (“maths”, 72) and so on.
The program itself is now short, because each step is a call to a module:
import marks_fileimport report students = marks_file.read_marks("marks.csv")for student in students: print(report.report_line(student, marks_file.SUBJECTS))Output
Mina Shrestha: science A, maths B+, computer A+ (passed)Bikash Tamang: science NG, maths C+, computer BSita Yadav: science A+, maths A+, computer A+ (passed)Pemba Sherpa: science B, maths NG, computer AHiding the insides
There is a deeper idea behind modules. Whoever uses a module needs to know only what its functions do: their names, what to give them, what they give back. That list is the module’s interface. How the functions do it, the code inside, can stay hidden, so the module’s writer can improve it, make it faster or fix it, without anyone else changing a line, as long as the interface stays the same. This is called information hiding, and you will see it work in the next section, when marks_file.py gets new insides and the program doesn’t notice.
Why use if __name__ == "__main__": in a module?
Pick an answer to see why.
Libraries: other people’s code
Ms. Karki now wants the class average, the middle mark and the spread of marks in each subject. She could write the functions. But somebody already has, tested them carefully, and given them away.
A library is a collection of modules written for other programs to use. Python comes with a large one, the standard library: hundreds of modules, installed with Python on every computer. Its makers describe Python as having “batteries included”. A few you have used, and a few more:
| Module | What it is for | Example |
|---|---|---|
| math | square roots, π, trigonometry | math.sqrt(81) |
| random | random choices | random.randint(1, 6) |
| time | clocks and timing | time.perf_counter() |
| statistics | averages and spread | statistics.median(marks) |
| datetime | dates and times | date(2026, 10, 23) |
| csv | reading and writing CSV files | csv.DictReader(file) |
| json | data in JSON, the language of APIs | json.loads(text) |
The statistics module
import statistics science = [63, 21, 72, 45]print("Average:", statistics.mean(science))print("Middle mark:", statistics.median(science))print("Spread:", round(statistics.stdev(science), 1))Output
Average: 50.25Middle mark: 54.0Spread: 22.5Three lines of work, each tested by Python’s own developers on far more cases than you would think of. The median is the middle value once the marks are in order (with four marks, halfway between the middle two, 45 and 63). The standard deviation, stdev, measures how spread out the marks are: a small one means most students scored near the average. You will need ideas like these when you draw conclusions from data in Unit 4.
Dates, and a lucky draw
from datetime import date exam = date(2027, 3, 18)today = date(2026, 10, 23)print((exam - today).days, "days to go")Output
146 days to godatetime knows the length of every month and every leap year, so subtracting two dates gives the number of days between them. (The exam date here is only an example.)
The school’s sports-day lucky draw needs three winners from the tickets sold, chosen fairly, with no ticket winning twice. random.sample does exactly that. Run it a few times: the winners change, because the choice is random.
import random tickets = list(range(101, 161)) # tickets 101 to 160winners = random.sample(tickets, 3)print("First prize:", winners[0])print("Second prize:", winners[1])print("Third prize:", winners[2])Using a library from its documentation
marks_file.py reads the marks by splitting each line at its commas. That breaks on real files. A student’s name written as “Shrestha, Mina” has a comma inside it, and spreadsheets save such a value in quotes, "Shrestha, Mina", which our split(",") would cut into two. The standard library’s csv module knows all the rules of CSV files. Here is how its documentation (docs.python.org, the page csv: CSV File Reading and Writing) introduces the tool we need, shortened:
class csv.DictReader(f, fieldnames=None, ...) Create an object that operates like a regular reader but maps the information in each row to a dict whose keys are given by the optional fieldnames parameter. If fieldnames is omitted, the values in the first row of file f will be used as the fieldnames ...Read it with Grade 8’s questions. f is the file, already opened. fieldnames is optional (it has a default, None); leave it out, and the first row’s values become the keys. Each row comes out as a dictionary, which is exactly what our program wants. One more thing, from an example further down the page: every value comes out as a string, even “63”, so the marks still need int().
Here is marks_file.py with new insides. Its interface hasn’t changed: the same function name, the same parameter, the same kind of list given back.
marks_file.py
"""Reading the class's marks from a CSV file.""" import csv SUBJECTS = ["science", "maths", "computer"] def read_marks(file_name): """A list of students from a marks file: one dictionary per row, like {"name": "Mina Shrestha", "science": 63, ...}.""" students = [] with open(file_name, newline="") as file: for row in csv.DictReader(file): for subject in SUBJECTS: row[subject] = int(row[subject]) students.append(row) return studentswith open(...) as file: (Grade 9) is the usual way to open a file in Python: it closes the file by itself at the end of the indented block, even if something goes wrong inside it. (newline="" is what the csv documentation asks for when a file is opened for the csv module.)
Now run the very same program as before, unchanged. It gets the new marks_file.py, the last one shown above it, and prints the same results:
import marks_fileimport report students = marks_file.read_marks("marks.csv")for student in students: print(report.report_line(student, marks_file.SUBJECTS))Output
Mina Shrestha: science A, maths B+, computer A+ (passed)Bikash Tamang: science NG, maths C+, computer BSita Yadav: science A+, maths A+, computer A+ (passed)Pemba Sherpa: science B, maths NG, computer AThat is information hiding at work: a module’s insides changed completely, and nothing that used it had to change.
Libraries from everywhere: PyPI and pip
Beyond the standard library there are hundreds of thousands of free libraries that programmers around the world have shared. Python’s are collected in one place, the Python Package Index, PyPI (pypi.org). On a computer with Python installed, a program called pip downloads a library from PyPI and installs it, with one command typed in the terminal (not in Python):
pip install nepali-datetimeLibraries from PyPI are called third-party libraries: written neither by you nor by Python’s own team, but by a third person.
Choosing a library safely
A library saves you work, but your program now depends on code you didn’t write. Before you add one, ask:
- Do I need it? For a few lines you can write and test yourself, a library may be more risk than help (the story below).
- Is it widely used and looked after? On its PyPI page: how recently a new version came out, whether it has documentation, whether people report problems and get answers. A library nobody has updated in years may not work with today’s Python, and its mistakes won’t be fixed.
- Is it the real one? Copy the name from the official documentation or PyPI page, letter by letter. Criminals upload harmful packages with names one letter away from popular ones, hoping someone mistypes; this has happened on PyPI many times. Installing a package runs its code on your computer.
- Does its license allow your use? Grade 9 showed how to read one Grade 9: Computing and the Law, and that most libraries’ licenses ask you to keep their credit.
Reading documentation
Grade 8 showed you help() and the parts of a documentation entry. With libraries, documentation becomes the way you learn: nobody remembers every function of hundreds of modules, and professionals look things up all day. Library documentation usually comes in two kinds:
- a tutorial or guide: a walk through the library, with examples, to learn how it is meant to be used;
- a reference: an entry for every function, to look up exactly what it takes and gives back.
A reference entry answers, in order: how to call it (its signature: the name and parameters, with defaults), what it does, what it returns, and what can go wrong: the errors it raises. Here is Python’s own help for random.sample, which you used above, shortened:
sample(population, k, *, counts=None) Chooses k unique random elements from a population sequence. Returns a new list containing elements from the population while leaving the original population unchanged. ... To choose a sample from a range of integers, use range() for the population argument.populationandkhave no default, so you must give both: the things to choose from and how many.- The
*is not a parameter. It means that every parameter after it must be given by name, as a keyword argument:counts=[2, 1], never just[2, 1]. You have used keyword arguments before:sorted(marks, reverse=True). - “unique” means no element is chosen twice, and “leaving the original population unchanged” tells you that it does not change the list it is given, unlike
top_three()earlier in this unit. Good documentation says so.
Exploring a module
Two built-in functions help you explore a module on your own. dir() lists the names a module has (leave out the ones starting with an underscore, which are its private insides), and help() shows one of them:
import statistics names = [n for n in dir(statistics) if not n.startswith("_")]print(names)help(statistics.mean)Among the names you will see mean, median and mode, and also StatisticsError. The help for mean ends: “If data is empty, StatisticsError will be raised.” That sentence is worth knowing before it happens.
When a function fails: try and except
What should the program do if a subject’s list of marks is empty, because no one took the exam yet? Or if a mark in the file is typed as “6O”, with the letter O? The documentation tells you what each function raises when something is wrong: statistics.mean([]) raises StatisticsError, and int("6O") raises ValueError. A program can be ready for an error it expects, with try and except:
import statistics def average(marks): """The mean of the marks, or None if there are none.""" try: return statistics.mean(marks) except statistics.StatisticsError: return None print(average([63, 21, 72, 45]))print(average([]))Output
50.25NonePython runs the lines under try. If one of them raises the error named after except, Python jumps straight to the lines under except, instead of stopping the program. If nothing goes wrong, the except part is skipped. Here a missing average becomes None, which the caller can check, and print “no marks yet”.
A function’s documentation says round_to(x, *, places=0). Which call is correct?
Pick an answer to see why.
APIs: asking other programs for data
When you open a weather app, the forecast isn’t worked out on your phone. The app asks a weather service’s computers, and they answer with the numbers. A bus-booking site asks each bus company’s computer which seats are free. A shop’s website asks a mobile wallet’s computer to take the payment, and gets back whether it went through. Programs everywhere ask other programs for things, and the rules for asking are written down.
An API (application programming interface) is the published list of what a program, library or service offers to other programs, and exactly how to ask for it. You have already used APIs: the functions a module offers, with their parameters and return values, are that module’s API, and its reference documentation describes it. A web API is the same idea across the internet: a program on another computer that answers requests sent to web addresses. It answers with data for a program to use, not a page for a person to read.
An address that asks a question
A request to a web API is a web address, built from parts:
The details after the ? are the request’s query parameters, the arguments of a web API. The API’s documentation lists the parameters it understands, like a function’s. The answer comes back with a status code, a number that says how it went: 200 means OK; 404, Not Found, means nothing is at that address; 400, Bad Request, means the request is wrong in a way the API can explain. With the status comes the data, usually in JSON.
Try an API
This practice API knows Nepal’s 77 districts and their populations in the 2021 census. It lives inside this page, so nothing is sent anywhere, but it behaves like a real one. Here is its documentation:
| Request | Answer |
|---|---|
/provinces |
the 7 provinces, each with its number of districts |
/districts?province=<name> |
the districts of one province, in alphabetical order |
/districts/<name> |
one district: its province, headquarters and population |
Each address starts with https://api.example.com/nepal/v1. (Addresses at example.com are kept for examples: no real API lives there.)
JSON: data that travels
JSON (JavaScript Object Notation) is a way of writing data as plain text, so that any program in any language can read it. It was made popular in the early 2000s by an American programmer, Douglas Crockford, who wrote its rules down on one short web page, and today most web APIs answer in it. It will look familiar, because it is almost the same as Python’s lists and dictionaries:
| JSON | Python |
|---|---|
{"name": "Bhaktapur"}, an object |
a dictionary |
["Koshi", "Madhesh"], an array |
a list |
"Bhaktapur", 432132, 3.5 |
a string, an int, a float |
true, false, null |
True, False, None |
The json module turns JSON text into Python’s dictionaries and lists, with json.loads() (“load from a string”), or json.load() to read it from a file. Here is a real answer from the Open-Meteo API for Bhaktapur, saved to a file on 24 October 2026 and shortened (weather data by Open-Meteo.com, CC BY 4.0):
bhaktapur-weather.json
{ "latitude": 27.662565, "longitude": 85.454544, "timezone": "Asia/Kathmandu", "elevation": 1332.0, "current_units": {"time": "iso8601", "temperature_2m": "°C"}, "current": {"time": "2026-10-24T05:15", "temperature_2m": 13.7}, "daily_units": {"time": "iso8601", "precipitation_probability_max": "%"}, "daily": { "time": ["2026-10-24", "2026-10-25"], "precipitation_probability_max": [8, 2] }}import json with open("bhaktapur-weather.json") as file: data = json.load(file) now = data["current"]print("At", now["time"], "it was", now["temperature_2m"], data["current_units"]["temperature_2m"]) days = data["daily"]["time"]rain = data["daily"]["precipitation_probability_max"]for day, chance in zip(days, rain): print(day, "- chance of rain:", str(chance) + "%")Output
At 2026-10-24T05:15 it was 13.7 °C2026-10-24 - chance of rain: 8%2026-10-25 - chance of rain: 2%Line 6 shows how to read nested data: data["current_units"] is a dictionary inside the answer, and ["temperature_2m"] is a key inside that. Each [ ] goes one step in, exactly as in the API widget. (The API answers for the nearest suitable point on its weather grid, here about 3 kilometers east of Bhaktapur’s Durbar Square, which is why its latitude and longitude aren’t the ones asked for.)
An API answers {"district": "Ilam", "tea_gardens": [{"name": "Kanyam", "area": 85}]}, loaded into data. Which reaches the 85?
Pick an answer to see why.
A real API: the weather in Bhaktapur, live
Now ask the real API. A program needs a way to fetch an address from the internet, and that is done differently on a computer (the standard library’s urllib.request, or the requests library) and on this page (Pyodide, the Python that runs inside a web browser, has its own function for it). So we hide the difference in a small module of our own, internet.py, with one function. Its insides choose the right way; its interface is the same everywhere.
internet.py
"""Fetching text from the internet, on this page or on a computer.""" def get_text(address): """The text found at a web address, such as an API's JSON answer. Raises OSError if the address can't be reached.""" try: from pyodide.http import open_url # Python in a web page from pyodide.ffi import JsException except ImportError: from urllib.request import urlopen # Python on a computer with urlopen(address) as answer: return answer.read().decode("utf-8") try: return open_url(address).read() except JsException: # the browser couldn't fetch it: raise OSError("Couldn't reach " + address) # the error a computer givesThe last two lines turn the web page’s own kind of error into the one Python on a computer raises when a connection fails, OSError. So a program that uses get_text has only one error to expect, wherever it runs. A line that starts with raise makes an error happen on purpose, as library functions do when something is wrong.
This program asks Open-Meteo for Bhaktapur’s weather now and the chance of rain in the next three days. It needs the internet, and its answer will be different every time:
import jsonimport internet place = "latitude=27.67&longitude=85.43"want = "current=temperature_2m&daily=precipitation_probability_max"address = ("https://api.open-meteo.com/v1/forecast?" + place + "&" + want + "&timezone=Asia/Kathmandu&forecast_days=3") try: data = json.loads(internet.get_text(address))except OSError: print("Couldn't reach the weather API. Are you online?")else: print("Bhaktapur now:", data["current"]["temperature_2m"], "°C") days = data["daily"]["time"] rain = data["daily"]["precipitation_probability_max"] for day, chance in zip(days, rain): print(day, "- chance of rain:", str(chance) + "%") print("Weather data by Open-Meteo.com")Two new things. The try has an else: its lines run only if nothing under try raised an error. And the error caught is OSError, the kind Python raises when a connection fails: no internet is something a program that uses an API must always expect. The last line credits the data’s source, which Open-Meteo’s license (CC BY 4.0) asks for.
Keys, limits and good manners
Real APIs come with rules, in their documentation and terms of use:
- Keys. Many APIs need an API key: a long secret string, given to you when you sign up, sent with every request so the service knows who is asking (and, for a paid API, whom to bill). Treat a key like a password. Never put it in code that you share or publish: anyone who copies it can use your account, and your allowance or your money.
- Limits. Most APIs allow only so many requests a minute or a day. A program that asks again for the same answer every second wastes everyone’s computers, and gets blocked. Ask once, keep the answer, and ask again only when it may have changed.
- Failures. The internet goes down, a server is busy, an API changes. A program that uses an API must check the status, catch the errors, and tell the user plainly, as ours does.
- Trust. Data from an API is only as good as whoever runs it. Check where it comes from, and credit it as its license asks.
Project: a program in modules
Build a program of your own, in at least three files: a main program and two modules of your own, using at least one module of the standard library from its documentation. Choose one:
- A tea shop’s bills. A menu module (items and prices, read from a CSV file), a billing module (a pure function for a bill’s total, with a discount for big orders), and a main program that takes an order and prints a bill.
- A library’s lending. A books module (the books and who has borrowed them, saved in a file), a dates module that uses
datetimeto work out when each book is due back and how many days late it is, and a main program for the librarian. - Ms. Karki’s results, finished. Add English and social studies, a class table sorted by average (best first), and each subject’s mean, median and highest mark from
statistics.
- Plan top-down. Write main() first, as calls to functions that don’t exist yet. Then list the functions, and decide which module each goes in (one job per module).
- Write the interfaces first. For every function: its name, parameters and a docstring saying what it returns. If you can’t write the docstring in one sentence, split the function.
- Keep most functions pure. Put reading, printing and saving in a few functions at the edges.
- Test each module by itself, with tests under
if __name__ == "__main__":that check the edges (an empty list, the exact pass mark, a mark of 0). - No globals that change. Constants in capitals are fine; everything else goes in through parameters and out through
return. - Swap a partner. Give your modules to a classmate with only their docstrings. Can they use them without reading the code inside? If not, improve the docstrings or the interface.
- Optional: add an API. Show tomorrow’s chance of rain on the tea shop’s bill, or on the class’s notice board, from Open-Meteo.
This needs Python on a computer, where a program can be several files in one folder: Thonny (free from thonny.org) is a good editor for it. With only a phone, you can still plan steps 1 and 2 on paper, and write and test each module’s functions one at a time in the programs on this page.
In other grades: Grade 8 Computing first wrote functions, with parameters, return values and a first look at local variables, in Functions: Building Blocks of Programs, and read documentation with help() in Reading documentation. Grade 9 Computing built lists of dictionaries and read CSV files in Lists, Strings and Dictionaries, and looked at the licenses of libraries in Computing and the Law. For the SEE, Grade 10 Computer Science covers scope in User-defined functions, libraries in Libraries and packages in Python, and try and except in Error handling. Grade 11 Computing groups data and functions together into objects, and its unit on clients and servers builds web APIs of its own.
In short
Exercise
In top-down design, what do you write first?
Pick an answer to see why.
Inside a function, the line count = count + 1 stops with UnboundLocalError. Why?
Pick an answer to see why.
What does from grading import grade_for do?
Pick an answer to see why.
Which is the best reason to use a well-known library from the standard library instead of writing the same function yourself?
Pick an answer to see why.
An API answers a request with the status code 404. What does it mean?
Pick an answer to see why.
A classmate puts their weather API key in a program and shares the program in a class group. What is the problem?
Pick an answer to see why.
Explain these words, with an example of each from this unit: module, library, namespace, side effect, API.
Answer
What does this program print? Explain each line of output using the idea of scope.
x = 10 def f(x): x = x * 2 return x def g(): return x + 1 print(f(3))print(g())print(x)Answer
Read this function as the unit showed (in, builds, out, side effects), and say in one sentence what it does. Then give it a better name and a docstring.
def h(students, subject): best = students[0] for s in students: if s[subject] > best[subject]: best = s return best["name"], best[subject]Answer
A student writes this function to give everyone 5 grace marks for a hard question, and checks it with science = [63, 21, 72], new = add_grace(science). Later the program prints science and shows the grace marks already added. Explain why, and rewrite the function so that the original list is not changed.
def add_grace(marks): for i in range(len(marks)): marks[i] = marks[i] + 5 return marksAnswer
Rewrite this program without the global line, so that add_sale is a pure function. Why is your version better?
total = 0 def add_sale(price, plates): global total total = total + price * plates add_sale(150, 2)add_sale(120, 3)print("Today's sales: Rs", total)Answer
What is the difference between import statistics and from statistics import mean? Write the line that prints the mean of marks after each.
Answer
A file shop.py ends with these lines. What do they do when shop.py is run by itself? And when a program runs import shop?
if __name__ == "__main__": print(bill_total(150, 2))Answer
You need a library to make QR codes for your school’s fee receipts. Searching PyPI, you find several. List four things you would check before choosing and installing one, and why each matters.
Answer
The documentation of a function says:
split_bill(total, people, *, tip=0): “Returns how much each person pays, rounded up to the next rupee. Raises ValueError if people is less than 1.”
- a.Which arguments must you give? Which can you leave out?
- b.Write a call for a Rs 1,000 bill shared by 3 friends with a Rs 50 tip.
- c.Write a few lines that call it for 0 people without the program stopping, and print “Nobody to pay!” instead.
Answer
A program asks a web API for a district’s information. Explain what each part of this address is for: https://api.example.com/nepal/v1/districts?province=Karnali. Then say what a program should do if the API answers 404, and if there is no internet at all.
Answer
Turn this JSON answer into Python, and write the lines that print Rara and 10.8.
{"province": "Karnali", "lakes": [{"name": "Rara", "area_km2": 10.8}, {"name": "Phoksundo", "area_km2": 4.9}]}Answer
A school’s program has 3,000 lines in one file, written by one teacher who has now left. Explain to the school’s new computer teacher how they could split it into modules, step by step, and why it is worth doing.
Answer
Quiz yourself: Algorithms and programming, Grades 6–12 →
Practice every question in the book, and keep track of the ones you miss →
