dolese TECH

IT Handbook

From your first web page to your first database

  • 01 Computer essentials
  • 02 HTML and CSS
  • 03 JavaScript
  • 04 Databases with SQL
  • 05 Python
  • 06 C++ essentials
  • 07 Staying safe online
  • 08 Git and GitHub

First edition, 2026 · dolese.tech

Contents

  1. 1Computer essentialsShortcuts, files and folders, symbols, and how the internet works.
  2. 2HTML and CSSBuild the results page of a small school system.
  3. 3JavaScriptCheck marks as they are entered, and work out averages, grades and positions.
  4. 4Databases with SQLStore students, subjects and marks, and ask questions of them.
  5. 5PythonImport a class list, and calculate grades and divisions automatically.
  6. 6C++ essentialsClasses, inheritance and the standard library, for college courses.
  7. 7Staying safe onlineProtect your devices, your accounts, your money and your work.
  8. 8Git and GitHubKeep every version of your project safe, share it, and publish it.
  9. AKiswahili glossaryEvery key term in the book, in English and Kiswahili.

Every chapter builds part of one project: a small school results system. Code examples are tested before they are printed, and examples use Tanzanian names, places and numbers.

Chapter 1

Computer essentials

Shortcuts, files and folders, symbols, and how the internet works.

Before you write a web page or a line of Python, you need to be comfortable with the machine itself: where your files are, how to move around quickly, which symbols are which on the keyboard, and what actually happens when you open a website. None of this is difficult, but people who skip it spend years losing files and hunting for keys. This chapter gives you the foundation every later chapter stands on.

By the end of this chapter you will be able to:

  • name the main parts of a computer and say what each one does;
  • organise your work in folders and always know where a file was saved;
  • read a file path and a file extension, and know why they matter;
  • use the keyboard shortcuts that save the most time;
  • find and name every symbol a programmer types;
  • explain in plain words how a web page travels from a server to your screen;
  • set up the folder for the project you will build through this book.

1.1 What is inside a computer

Every computer — a laptop, a desktop in the school office, or the phone in your pocket — is made of two things. Hardware is the part you can touch: the screen, the keyboard, the chips inside. Software is the set of instructions that tells the hardware what to do: Windows, Android, Microsoft Word, WhatsApp, and the programs you will write in this book.

The table shows the hardware parts that matter most when you choose, use or repair a computer.

PartWhat it doesWhat to look for
Processor (CPU)Carries out instructions — the “brain” of the computerNewer generations are faster; Intel Core i3/i5 or AMD Ryzen 3/5 are fine for learning
Memory (RAM)Holds the programs and files you are using right now; emptied when the power goes off8 GB is comfortable for programming; 4 GB works but feels slow
Storage (SSD or hard disk)Keeps your files and programs when the power is offAn SSD starts the computer and opens programs several times faster than an old hard disk
ScreenShows outputA larger screen makes it easier to see code and a browser side by side
Keyboard and mouseInput: how you give the computer instructionsA full keyboard with arrow keys is easier for programming
BatteryKeeps a laptop running without mains powerImportant where power cuts are common

A useful way to remember the difference between memory and storage: memory is your desk, storage is your cupboard. You take a file out of the cupboard and put it on the desk to work on it. The desk is fast but small, and it is cleared every evening. Anything you want to keep must go back into the cupboard — which is exactly what Save does.

The operating system

The operating system is the software that runs everything else. It starts the computer, manages files, and lets programs share the screen, the keyboard and the internet connection. Most computers in Tanzanian schools and offices run Windows; most phones run Android. Many servers on the internet — including the ones that will host your web pages — run Linux. The ideas in this book work on all of them, and where the steps differ we show the Windows way first.

1.2 Files and folders

A file is one saved piece of work: a letter, a photo, a spreadsheet of marks, a page of code. A folder is a container that holds files and other folders. Good folders are the difference between finding last term’s marks in ten seconds and never finding them at all.

Organise by purpose, not by date

Decide on a small number of top-level folders and keep to them. A teacher’s Documents folder might look like this:

Documents
├── School
│   ├── 2026
│   │   ├── Form 2 Biology
│   │   │   ├── Schemes of work
│   │   │   ├── Tests
│   │   │   └── Marks
│   │   └── Form 4 Biology
│   └── Staff meetings
├── Personal
└── Projects
    └── results-system

Each folder answers one question: which year? which class? what kind of document? When you save a file, you already know where it belongs.

Naming files well

A good file name tells you what is inside without opening it.

Poor nameBetter nameWhy
New Document (3).docxform-2-biology-test-march-2026.docxSays the class, subject, kind of document and date
final FINAL 2.xlsxform-4-marks-term-1-v2.xlsxA version number instead of “final” — there is always another final
IMG_20260314_0915.jpgscience-fair-2026-poster.jpgRename photos you plan to use
my page.htmlresults.htmlWeb files work best with no spaces

For anything you will use on the web or in code — and that is most of this book — follow three rules: use lowercase letters, use hyphens instead of spaces, and avoid symbols such as #, &, ? and %. A space in a web address becomes %20, and a # or ? changes the meaning of the address completely.

File extensions

The letters after the last dot in a file name are its extension. The extension tells the computer which program should open the file.

ExtensionKind of fileOpens with
.docxWord documentMicrosoft Word, LibreOffice Writer
.xlsxSpreadsheetMicrosoft Excel, LibreOffice Calc
.pdfDocument that looks the same everywhereA browser, or any PDF reader
.jpg, .pngPicturePhotos, any browser
.txtPlain text with no formattingNotepad, any code editor
.html, .css, .jsWeb page, its styles, its JavaScriptA browser to view; a code editor to change
.pyPython programPython; a code editor to change
.cppC++ programA C++ compiler; a code editor to change
.sqlDatabase commandsA database tool
.exeWindows program that runs when openedWindows itself — be careful
.zipSeveral files packed into oneWindows Explorer, 7-Zip

Windows hides extensions by default, which causes two problems. You cannot tell results.html from results.html.txt, so a web page you wrote in Notepad refuses to open in the browser. And a harmful program called photo.jpg.exe appears on screen as photo.jpg. Turn extensions on now; you will need them for every chapter that follows.

Paths

Every file has an address called its path: the drive, then each folder in turn, then the file name. On Windows the parts are separated by a backslash \:

C:\Users\Amina\Documents\School\2026\Form 2 Biology\Marks\term-1.xlsx

Read it from left to right: drive C:, then the folder Users, then Amina, and so on, down to the file term-1.xlsx. Web addresses and Linux computers use a forward slash / instead, and so does most code — even on Windows:

https://example.com/school/2026/results.html

You can copy the full path of any file in File Explorer: hold Shift, right-click the file and choose Copy as path.

Where did it save?

The most common computer problem is not a virus — it is a file saved “somewhere”. Three habits prevent it:

  1. Read the Save dialog before you click Save. The box at the top shows which folder you are in. Change it if it is wrong.
  2. Check Downloads. Files from the internet, email and WhatsApp Web go to the Downloads folder unless you choose otherwise. Move them to the right folder the same day.
  3. Use search. Press the Windows key and start typing part of the file name. In File Explorer, the search box at the top right searches the folder you are in and everything inside it.

1.3 Keyboard shortcuts

A shortcut is a combination of keys that does a job without the mouse. You hold the first key — usually Ctrl — and tap the second. People who use shortcuts are not showing off; they are simply faster, and their hands stay on the keyboard while they type code.

You do not need to learn every shortcut at once. Learn the first table this week, and add a few from the others each week.

The ones you will use every day

These work in almost every program on Windows: Word, Excel, browsers and code editors.

ShortcutWhat it does
Ctrl + CCopy the selected text or file
Ctrl + XCut — copy it and remove it from here
Ctrl + VPaste what you copied or cut
Ctrl + ZUndo the last change — press again to go further back
Ctrl + YRedo what you just undid
Ctrl + SSave
Ctrl + ASelect everything
Ctrl + FFind a word on the page or in the document
Ctrl + PPrint

On a Mac, use the Cmd key wherever these tables say Ctrl.

Moving around Windows

ShortcutWhat it does
Alt + TabSwitch between open windows — keep holding Alt and tap Tab to choose
Windows + EOpen File Explorer
Windows + DShow the desktop; press again to bring the windows back
Windows + LLock the computer — do it every time you walk away
Windows + VClipboard history: paste something you copied earlier, not just the last thing (turn it on the first time)
Windows + Shift + STake a screenshot of part of the screen
Windows + ← / →Snap a window to the left or right half of the screen
Ctrl + Shift + EscOpen Task Manager, to close a program that has stopped responding
F2Rename the selected file

In the browser

ShortcutWhat it does
Ctrl + TOpen a new tab
Ctrl + WClose the current tab
Ctrl + Shift + TReopen the tab you closed by mistake
Ctrl + LJump to the address bar, ready to type
Ctrl + R or F5Reload the page — you will do this after every change to your web pages
Ctrl + + / Ctrl + -Make the page bigger or smaller; Ctrl + 0 resets it
F12Open the developer tools, which you will use from Chapter 2

When editing text

ShortcutWhat it does
Ctrl + ← / →Move the cursor one whole word left or right
Home / EndJump to the start or end of the line
Shift + arrow keysSelect text as you move
Ctrl + Shift + ← / →Select whole words at a time
Ctrl + BackspaceDelete the whole word before the cursor
Ctrl + Home / Ctrl + EndJump to the very start or very end of the document

1.4 The symbols programmers type

Code uses symbols that you rarely type in an ordinary letter. Many beginners lose time searching the keyboard for them, or mix up two that look alike. Learn their names: when someone says “put it in curly braces”, you should know at once which keys to press.

The table is for the standard US keyboard layout, which almost every computer sold in Tanzania uses.

SymbolNameHow to type itWhere you will meet it
( )Parentheses, round bracketsShift + 9, Shift + 0Every language: print("Habari")
[ ]Square bracketsThe keys to the right of PLists in Python and JavaScript
{ }Curly bracesShift + the keys to the right of PCSS rules; blocks in JavaScript and C++
< >Angle brackets, less than / greater thanShift + , and Shift + .HTML tags; comparisons such as marks > 40
;SemicolonThe key to the right of LEnds a statement in JavaScript, C++, SQL and CSS
:ColonShift + ;CSS properties; Python if and for lines
" 'Double and single quotesThe key to the left of EnterAround text: "Dodoma"
`BacktickThe key to the left of 1JavaScript template text; Markdown code
~TildeShift + the backtick keyYour home folder in a terminal
/Forward slashThe key to the left of right ShiftWeb addresses; division; comments //
\BackslashThe key above EnterWindows paths; special characters such as \n
|Vertical bar, pipeShift + \“Or” in code: ||; tables in Markdown
_UnderscoreShift + -Names in Python and SQL: student_name
-Hyphen, minusThe key to the right of 0Subtraction; file names for the web
=Equals signThe key to the left of BackspaceGives a value: total = 0
==Double equalsPress = twiceAsks “are these equal?” — not the same as =
#HashShift + 3Python comments; CSS ids; Markdown headings
%PercentShift + 5The remainder after division: 7 % 2 is 1
&AmpersandShift + 7“And” in code: &&
!Exclamation markShift + 1“Not” in code: != means “is not equal to”
*Asterisk, starShift + 8Multiplication; “everything” in SQL
^CaretShift + 6Rarely; it does not mean “to the power of” in most languages
@At signShift + 2Email addresses; CSS rules such as @media
$Dollar signShift + 4Inside JavaScript template text: ${name}

Pairs that are easy to confuse

  • / and \. The forward slash leans forward like a runner; the backslash leans back. Web addresses only ever use /.
  • = and ==. One equals sign puts a value into a name. Two compare two values. Mixing them up is one of the most common beginner mistakes in every language.
  • 0 and O, 1 and l. The digit zero and the capital letter O look alike in many fonts, as do the digit one and a lowercase L. Code editors use fonts that make them different — look closely when you copy code.
  • Straight and curly quotes. Word processors change " into “ and ” as you type. Code needs the straight ones. If you copy code from a Word document or a PDF and it fails for no reason, the quotes are the first thing to check.

1.5 Typing well

You will type a lot of code. If you look at the keyboard for every letter, each line takes three times as long and you make more mistakes. Ten minutes of touch-typing practice a day for a month is one of the best investments a new programmer can make.

  • Rest your fingers on the home row: left hand on A S D F, right hand on J K L ;. The small bumps on F and J let you find them without looking.
  • Each finger is responsible for the keys above and below its home key.
  • Use free practice websites such as typing.com or keybr.com. Aim for accuracy first; speed follows.
  • Learn where the programmer’s symbols are from the table above, and practise typing lines of real code, not just sentences.

1.6 How the internet works

You use the internet every day, but programmers need to know what happens underneath. When you understand the journey of a web page, error messages stop being mysterious, and Chapters 2 to 4 make much more sense.

Clients and servers

The internet is a huge network of computers connected to each other. Most of them play one of two roles:

  • A client asks for something. Your browser — Chrome, Edge, Firefox, Safari — is a client.
  • A server answers. It is a computer, usually in a data centre, that stores websites and sends them to anyone who asks.

Every time you open a page, your browser sends a request to a server, and the server sends back a response: the page itself, its pictures, its styles and its scripts.

Addresses: IP addresses and domain names

Every device on the internet has a number called an IP address, such as 203.0.113.10. Numbers like that are hard to remember, so we give servers names instead: example.com, necta.go.tz, wikipedia.org. These names are domain names.

When you type a domain name, your device first asks a DNS server — the internet’s phone book — which IP address belongs to that name. Only then does your browser contact the right server. If DNS fails, you see an error such as “This site can’t be reached” even though your internet connection is working.

The parts of a web address

A web address is called a URL. Each part has a job.

https://shule.example.co.tz/results/form-4?year=2026#mathematics
PartIn the exampleWhat it means
Protocolhttps://How to talk to the server. The s means secure: the connection is encrypted
Subdomainshule.A section of a larger site, chosen by the owner of the domain
Domainexample.co.tzThe name the owner registered. Read it from the right: .tz is Tanzania, .co.tz is a company in Tanzania
Path/results/form-4Which page on the site — like a file path, with forward slashes
Query?year=2026Extra information sent to the server, as name=value pairs
Fragment#mathematicsA place on the page to jump to; it is not sent to the server

Chapter 7 shows how attackers abuse these parts to make fake addresses look real. The rule from that chapter is worth repeating here: the domain is the part just before the first single /, read from the right-hand end.

HTTP and HTTPS

The language browsers and servers use to talk to each other is HTTP. HTTPS is the same language inside an encrypted connection, so that no one in between — on the café Wi-Fi, or at your internet provider — can read or change what you send and receive. Every website that asks for a password or a payment must use HTTPS, and today almost all websites do.

What the server answers

Every response starts with a three-digit status code that says how the request went. You will see these often once you build your own sites.

CodeMeaningWhen you see it
200OKEverything worked — you never notice this one
301Moved permanentlyThe page has a new address; the browser follows it automatically
404Not foundThe address is wrong, or the page was removed
403ForbiddenThe page exists but you are not allowed to see it
500Server errorSomething broke on the server — not your fault as a visitor
503Service unavailableThe server is overloaded or down for maintenance

Codes that start with 4 mean the request had a problem; codes that start with 5 mean the server had a problem. When you build pages in Chapter 2 and see a 404, check the spelling of the file name and the folder it is in.

The journey of one page

Put together, opening https://example.com/results.html goes like this:

  1. You type the address and press Enter.
  2. Your device asks DNS for the IP address of example.com.
  3. Your browser opens a secure connection to that server.
  4. It sends a request: “Please send me /results.html.”
  5. The server finds the file and replies 200 OK with the HTML.
  6. The browser reads the HTML, sees that the page also needs a stylesheet, some images and a script, and requests each of them the same way.
  7. It draws the finished page on your screen.

All of this usually takes less than a second. In Chapter 2 you will write the HTML from step 5 yourself.

1.7 Data: bytes, bundles and saving

Everything a computer stores or sends is measured in bytes. One byte holds about one letter of text. Larger amounts use the units in the table; each is about a thousand times the one before.

UnitSizeRoughly how much
Byte (B)—One letter
Kilobyte (KB)About 1,000 bytesA short SMS conversation; a small HTML page
Megabyte (MB)About 1,000 KBOne photo from a phone (2–5 MB); one song (3–5 MB)
Gigabyte (GB)About 1,000 MBA typical data bundle; about an hour of video at standard quality
Terabyte (TB)About 1,000 GBA large external hard disk

You will sometimes see 1,024 used instead of 1,000. Both are in use; the difference does not matter for everyday decisions.

The same units explain your data bundle. Pages that are mostly text cost very little. Video and automatic updates are what empty a bundle quickly. To make a bundle last:

  • download large files and updates on Wi-Fi when you can;
  • in your phone’s settings, set app updates to install on Wi-Fi only;
  • lower the video quality in YouTube and similar apps;
  • watch for apps that use data in the background — Android shows data use per app under Settings → Network → Data usage (the exact names differ between phones).

1.8 Set up your project folder

Throughout this book you will build one project, step by step: a small school results system that stores students, subjects and marks, works out grades and divisions, and shows the results on a web page. Each chapter adds a piece.

Create its folder now, so that every file has a home from the first day:

  1. Open File Explorer (Windows + E) and go to Documents.
  2. Create a folder called Projects if you do not have one: right-click an empty area, choose New → Folder, type the name and press Enter.
  3. Inside Projects, create a folder called results-system — lowercase, with a hyphen, no spaces.
  4. Inside results-system, create these folders:
results-system
├── web        (Chapters 2 and 3: HTML, CSS and JavaScript)
├── database   (Chapter 4: SQL)
├── python     (Chapter 5)
└── cpp        (Chapter 6)
  1. Click once in the address bar at the top of the results-system window to see its full path, and write it down. You will need it when you open the project in your code editor.

In Chapter 8 you will put this whole folder under version control with Git, so that every change is saved and nothing you build can be lost.

Key terms

EnglishKiswahiliMeaning
HardwareMaunziThe physical parts of a computer
SoftwareProgramuInstructions that tell the hardware what to do
Operating systemMfumo wa uendeshajiThe software that runs the computer and all other programs
Memory (RAM)KumbukumbuFast, temporary space for the work you are doing now
StorageHifadhiWhere files are kept when the power is off
FileFailiOne saved piece of work
FolderKabrasha (folda)A container for files and other folders
KeyboardKibodiThe keys you type with
ShortcutNjia ya mkatoA key combination that does a job without the mouse
BrowserKivinjariThe program you use to open websites
WebsiteTovutiA collection of web pages under one domain name
ServerSevaA computer that stores websites and answers requests
DownloadPakuaCopy a file from the internet to your device
UploadPakiaCopy a file from your device to the internet

Check your understanding

  1. You were typing a report when the power went off, and the changes are gone. Using the “desk and cupboard” idea, explain what happened and how to prevent it.
  2. A student says their web page index.html will not open in the browser, and Notepad shows it was saved as a text file. What is the most likely problem, and what setting would have made it obvious?
  3. Write a better name for a file called Copy of marks NEW (2).xlsx that holds Form 3 Chemistry marks for Term 2 of 2026.
  4. What is the difference between total = 50 and total == 50?
  5. Name the symbols { }, [ ] and \, and give one place each is used.
  6. In https://maktaba.example.ac.tz/books?subject=history#top, which part is the domain, which is the path, and which is the query?
  7. You open a page and see 404. Is the problem more likely with the address you typed or with the server? What about 500?
  8. Why does watching videos use far more of a data bundle than reading news websites?

Answers

  1. Unsaved changes are kept only in memory — the “desk” — which is cleared when the power goes off. Only what was saved to storage — the “cupboard” — survives. Press Ctrl + S often.
  2. It was probably saved as index.html.txt. Turning on File name extensions in File Explorer would have shown the hidden .txt. In Notepad, choose Save as type: All files and type the full name index.html.
  3. For example, form-3-chemistry-marks-term-2-2026.xlsx.
  4. total = 50 puts the value 50 into total. total == 50 asks whether total is equal to 50, and gives true or false.
  5. { } are curly braces, used for CSS rules and code blocks in JavaScript and C++. [ ] are square brackets, used for lists in Python and JavaScript. \ is a backslash, used in Windows paths.
  6. The domain is example.ac.tz (with maktaba as its subdomain), the path is /books, and the query is ?subject=history. #top is the fragment.
  7. A 404 usually means the address is wrong or the page no longer exists. A 500 means something went wrong on the server.
  8. Video sends many pictures every second, which adds up to hundreds of megabytes an hour. A news page is mostly text, which is very small.
Chapter 2

HTML and CSS

Build the results page of a small school system.

Every website you have ever opened is built from two languages. HTML says what is on the page: this is a heading, this is a table of marks, this is a button. CSS says how it looks: navy here, green there, this much space between rows. In this chapter you will use both to build the first real piece of the project — the results page of a school, with a header, a summary, a table of marks with coloured grades, and a form for entering new marks. It will work on a computer, on a phone and on paper.

By the end of this chapter you will be able to:

  • install a code editor and open your project in it;
  • write a complete HTML page and view it in a browser;
  • use headings, paragraphs, lists, links, images, tables and forms;
  • explain what elements, tags and attributes are, and nest them correctly;
  • write CSS rules that change colours, fonts, spacing and layout;
  • arrange parts of a page side by side with flexbox;
  • make a page that works on a phone and prints cleanly;
  • find your own mistakes with the browser’s developer tools.

All the schools, teachers and students in this book are invented. Any resemblance to a real school is a coincidence.

2.1 Two languages, one page

Think of a web page as a classroom block being built. HTML is the structure: the walls, the doors, the windows, which room is which. CSS is the finish: the paint, the size of the windows, where the noticeboard hangs. You can have a building with no paint, and it still works — and you can have a page with no CSS, and it still works. But you cannot paint a wall that was never built.

That is why we write the HTML first and the CSS second, and why we keep them in separate files:

FileLanguageJob
results.htmlHTML (HyperText Markup Language)What is on the page, and what each part is
style.cssCSS (Cascading Style Sheets)How each part looks

Neither HTML nor CSS is a programming language in the full sense — they do not make decisions or do calculations. That arrives with JavaScript in Chapter 3. But every web developer, however experienced, writes HTML and CSS every day.

2.2 Your tools

You need two programs: a code editor to write the files, and a browser to view them.

Install Visual Studio Code

Visual Studio Code (VS Code) is a free code editor from Microsoft. It colours your code so that mistakes stand out, closes tags for you, and works for every language in this book.

  1. Open code.visualstudio.com in your browser — check the address carefully, as Chapter 7 taught you.
  2. Download the installer for Windows and run it. Accept the defaults, and tick Add “Open with Code” action when it is offered.
  3. Start VS Code. Choose File → Open Folder…, find the results-system folder you created in Chapter 1, and open it.
  4. In the panel on the left, open the web folder. Click the New File icon and name the file results.html.

Viewing your page

Find results.html in File Explorer and double-click it. It opens in your browser, and the address bar shows a path that starts with file:/// — the browser is reading the file straight from your disk, with no server involved. Arrange VS Code and the browser side by side (Windows + ← and Windows + →). Each time you change the file, switch to the browser and press Ctrl + R to reload.

2.3 Your first page

Type this into results.html. Type it rather than copying it: your fingers learn where the angle brackets and slashes are.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Form 4 Results</title>
</head>
<body>
  <h1>Mwangaza Secondary School</h1>
  <p>Form 4 results will appear here.</p>
</body>
</html>

Save and open it in the browser. You will see a large heading and one line of text. The tab at the top of the browser shows “Form 4 Results”.

Every HTML page you ever write starts with the same skeleton. Here is what each part does.

LineWhat it means
<!DOCTYPE html>“This is a modern HTML page.” Always the very first line
<html lang="en">The start of the page. lang="en" says the text is in English, which helps screen readers and translation tools. A page in Kiswahili would use lang="sw"
<head> … </head>Information about the page. Nothing in here appears in the page itself
<meta charset="utf-8">The character set. UTF-8 can show every letter and symbol in every language
<title>The text in the browser tab, in bookmarks and in search results
<body> … </body>Everything the visitor sees

2.4 Elements, tags and attributes

HTML is made of elements. Most elements have an opening tag, some content and a closing tag:

<a href="https://www.necta.go.tz">necta.go.tz</a>
  • <a ...> is the opening tag. a is the element’s name — a for anchor, which makes a link.
  • href="https://www.necta.go.tz" is an attribute: extra information about the element, written as name="value" inside the opening tag. Here it says where the link goes.
  • necta.go.tz is the content — the words the visitor sees and clicks.
  • </a> is the closing tag. It is the same name with a forward slash in front.

A few elements have no content and no closing tag, because there is nothing to put inside them. They are called void elements. You have already met <meta>; others are <img>, <input>, <link> and <br>.

Nesting

Elements go inside other elements. The rule is simple: close them in the reverse order you opened them, like boxes inside boxes.

<p>Class teacher: <strong>Mwl. Grace Mushi</strong></p>

The <strong> opens inside the <p>, so it must close before the </p>. Writing <p><strong>…</p></strong> is wrong, and browsers guess the fix in ways you may not expect.

Indent the inside of each element by two spaces. The browser does not care, but you will: with indentation you can see at a glance what is inside what. VS Code indents for you as you type.

The elements you will use most

ElementWhat it isExample
h1 to h6Headings, from most to least important<h2>Results</h2>
pA paragraph<p>Class teacher: …</p>
strong, emImportant text (bold); emphasised text (italic)<strong>Mwl. Grace Mushi</strong>
aA link<a href="page.html">Next</a>
imgAn image<img src="logo.png" alt="School badge">
ul, ol, liA bulleted list, a numbered list, and one item in either<li>5 students</li>
table, tr, th, tdA table, a row, a heading cell, an ordinary cellSee section 2.8
form, label, input, select, buttonA form and its partsSee section 2.9
header, main, section, footerThe large parts of a pageSee section 2.10
div, spanPlain containers with no meaning, used for styling<div class="table-wrap">

2.5 Headings and text

Replace the body of your page with a proper header. We will add class attributes now — they mean nothing yet, but in section 2.11 the CSS will use them.

<header class="site-header">
  <img src="logo.png" alt="Mwangaza Secondary School badge" width="56" height="56">
  <div>
    <h1>Mwangaza Secondary School</h1>
    <p>P.O. Box 1234, Dodoma</p>
  </div>
</header>

Use headings for their meaning, not their size. A page has one h1, its main title. The sections under it use h2; parts of those sections use h3. Never skip a level to get smaller text — that is CSS’s job. Search engines and screen readers for blind visitors use the headings as the page’s table of contents.

2.6 Lists

Below the header, the page opens with three numbers a head teacher wants to see first. They are a list of separate facts, so they go in a list.

<ul class="stats">
  <li><span>5</span> students</li>
  <li><span>66.3</span> class average</li>
  <li><span>Neema Kileo</span> top student</li>
</ul>

ul is an unordered list — the browser shows bullets. Use ol, an ordered list, when the order matters, such as steps to follow; the browser numbers the items for you. Each <span> marks the number inside its item so that CSS can make it large and green later.

Links

The href of a link can be written in two ways.

  • An absolute address includes everything, starting with https://. Use it for other websites: <a href="https://www.necta.go.tz">necta.go.tz</a>.
  • A relative address is measured from the folder the current page is in. Use it for your own pages: <a href="subjects.html">Subjects</a> opens subjects.html from the same folder.

Images

<img src="logo.png" alt="Mwangaza Secondary School badge" width="56" height="56">
  • src is the image file, with a relative or absolute address, exactly like href.
  • alt is the alternative text: what the image shows, in words. Screen readers read it aloud, and the browser shows it if the image cannot load. Every image needs one.
  • width and height are the size in pixels. Giving them lets the browser keep space for the image before it arrives, so the page does not jump about while loading.

Put a small picture called logo.png in your web folder to see it work. If you have no picture yet, leave the line in: the browser shows the alt text instead, which is exactly what it is for.

Relative addresses follow the folders on your disk:

srcFinds the file
logo.pngIn the same folder as the page
images/logo.pngIn a folder called images inside the page’s folder
../logo.pngIn the folder one level above the page’s folder

Keep images small

In Chapter 1 you saw that one phone photo can be 2 to 5 MB, and that many of your visitors pay for every megabyte. An image on a web page rarely needs to be more than about 1,600 pixels wide, and a badge like this one needs about 112. Before you put a photo on a page:

  • resize it to the size it will be shown at (Windows Photos: … → Resize image);
  • save photos as .jpg and logos and drawings as .png;
  • aim for under 200 KB per photo. Free websites such as squoosh.app make images much smaller with no visible difference;
  • add loading="lazy" to images further down a long page, so they are only downloaded if the visitor scrolls to them.

2.8 Tables

A table is the right element for data with rows and columns — and marks are exactly that.

<table>
  <caption>Marks out of 100. Grades: A 75-100, B 65-74, C 45-64, D 30-44, F 0-29.</caption>
  <thead>
    <tr>
      <th scope="col">No.</th>
      <th scope="col">Student</th>
      <th scope="col">Mathematics</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Amina Juma</td>
      <td>78</td>
    </tr>
  </tbody>
</table>
ElementMeaning
tableThe whole table
captionA title or note for the table, shown above it
theadThe heading rows
tbodyThe body rows — the data itself
trOne table row
thA heading cell. scope="col" says it is the heading of its column
tdAn ordinary table data cell

Every row must have the same number of cells as the heading row. If one row has a cell too many or too few, the columns slide out of line — count them when a table looks wrong.

The complete page in section 2.20 has eight columns — number, name, sex, three subjects, average and grade — and five students. The averages are worked out by hand for now: Amina’s is (78 + 85 + 81) ÷ 3 = 81.3. Doing that by hand for a whole school is slow and easy to get wrong, which is exactly what Chapter 3 fixes.

2.9 Forms

Teachers need to enter marks, so the page gets a form.

<form>
  <label for="student">Student</label>
  <select id="student" name="student">
    <option>Amina Juma</option>
    <option>Baraka Mushi</option>
  </select>

  <label for="mark">Mark (0 to 100)</label>
  <input id="mark" name="mark" type="number" min="0" max="100" required>

  <button type="submit">Save mark</button>
</form>
  • A label names a box. Its for attribute must match the id of the box it belongs to. Then clicking the label puts the cursor in the box, and screen readers announce the label when the box is reached.
  • select makes a drop-down list. Each option is one choice.
  • input makes a box to type in. Its type decides what kind: text, number, email, tel, date, password and more. On a phone, type="number" opens the number keypad.
  • min, max and required are checks the browser does for you. Try it: type 150 and click Save mark. The browser refuses and explains why, without a single line of programming.
  • An id must be unique on the page; no two elements may share one.

For now, clicking Save mark with a valid mark just reloads the page — there is nowhere for the mark to go yet. Chapter 3 catches the click with JavaScript, and Chapter 4 gives the marks a database to live in.

2.10 The structure of the page

Put the parts of the page in the large structural elements:

<body>
  <header class="site-header">…</header>
  <main>
    <section>…summary…</section>
    <section>…results table…</section>
    <section class="entry">…form…</section>
  </main>
  <footer>…</footer>
</body>
ElementUse it for
headerThe top of the page: name, badge, often the menu
mainThe main content. One per page
sectionA group of related content, normally with its own heading
footerThe bottom of the page: contact details, links, copyright

These look exactly like div elements until you style them, but they tell screen readers and search engines how the page is organised. A blind teacher using a screen reader can jump straight to main, or from section to section.

Your page now has all of its content. Open it in the browser: it is plain black text on white, with a table that has no lines. Everything is there — it just needs the finish.

2.11 Adding CSS

Create a second file in the web folder called style.css, and connect it to the page by adding this line inside the head of results.html:

<link rel="stylesheet" href="style.css">

The shape of a rule

CSS is a list of rules. Each rule chooses some elements and says how they should look.

.site-header h1 {
  margin: 0;
  font-size: 1.5rem;
}
  • .site-header h1 is the selector: which elements the rule applies to.
  • Inside the curly braces are declarations, each a property, a colon, a value and a semicolon.
  • /* like this */ is a comment. The browser ignores it; people reading your CSS do not.

Choosing elements: selectors

SelectorChoosesExample
pEvery element of that kindlabel { font-weight: 600; }
.nameEvery element with class="name".num { text-align: right; }
#nameThe one element with id="name"#mark { … }
a bEvery b inside an a.site-header p { … }
a, bBoth a and bth, td { … }
:hoverAn element while the mouse is over itbutton:hover { … }
:nth-child(even)Every second element: the 2nd, 4th, 6th…tbody tr:nth-child(even) { … }
*Every element* { box-sizing: border-box; }

Classes are the selector you will use most. One element can have several classes, separated by spaces: <span class="grade grade-a"> has both grade and grade-a, so the general badge style and the green colour both apply.

Why “cascading”

When two rules set the same property on the same element, the browser has to choose. Broadly: a more specific selector wins over a less specific one (an id beats a class, and a class beats an element name), and if they are equally specific, the rule that comes later in the file wins. This is the cascade. When a style you wrote “does nothing”, another rule is usually winning — section 2.18 shows how to see which.

2.12 Colours and text

Start style.css with the rules for the whole page:

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: "Segoe UI", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  color: #1b2435;
  background: #f4f6f8;
}
  • Colours are most often written as a # followed by six hexadecimal digits: two each for red, green and blue, from 00 (none) to ff (full). #ffffff is white, #000000 is black, and #1b2435 is a dark navy — a little red, a little more green, a fair amount of blue. Colour pickers in VS Code and in the browser’s developer tools write these for you.
  • font-family lists fonts in order of preference. The browser uses the first one the computer has. Always end with a general family — sans-serif or serif — as the last resort.
  • line-height: 1.5 makes each line one and a half times as tall as the text, which is much easier to read than the browser’s default.
  • rem is a unit equal to the page’s base font size, here 16 pixels. 1.5rem is 24 pixels. Sizing text in rem keeps everything in proportion if a reader makes the text larger.

Text colours must stand out clearly from their background. Dark text on a light background, or white text on a dark one, is readable in sunlight on a cheap phone; grey on grey is not.

2.13 The box model

To CSS, every element is a rectangular box made of four layers, from the inside out:

┌─────────────────── margin ───────────────────┐
│  ┌──────────────── border ───────────────┐   │
│  │  ┌───────────── padding ──────────┐   │   │
│  │  │                                │   │   │
│  │  │            content             │   │   │
│  │  │                                │   │   │
│  │  └────────────────────────────────┘   │   │
│  └───────────────────────────────────────┘   │
└──────────────────────────────────────────────┘
LayerWhat it isExample
ContentThe text or image itselfmax-width: 960px;
PaddingSpace inside the border, around the content. Takes the background colourpadding: 20px;
BorderA line around the paddingborder: 1px solid #dde2e8;
MarginSpace outside the border, between this box and its neighboursmargin-bottom: 24px;

The sections of our page are white boxes on a grey background:

main {
  max-width: 960px;
  margin: 0 auto;
  padding: 24px;
}

section {
  background: #ffffff;
  border: 1px solid #dde2e8;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 24px;
}

max-width: 960px stops the content stretching across a very wide screen, where lines become too long to read. margin: 0 auto means “no margin at the top and bottom, and share the remaining space equally left and right” — which centres the box. border-radius rounds the corners.

The * { box-sizing: border-box; } rule at the top of the file makes widths include the padding and border. Without it, a box set to 320 pixels wide with 8 pixels of padding would actually be 336 pixels wide, which surprises everyone.

2.14 Side by side with flexbox

Elements such as div, p, section and li are blocks: each starts on a new line and stacks under the one before. To put things side by side, give their container display: flex.

.site-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 24px;
  background: #1b2435;
  color: #ffffff;
}

Now the badge and the school name sit in a row. align-items: center lines them up through their middles, and gap puts 16 pixels between them.

The three numbers at the top use the same idea, with one addition:

.stats {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stats li {
  flex: 1 1 160px;
  padding: 12px;
  background: #eef8ee;
  border-radius: 6px;
}

flex: 1 1 160px tells each item: start at 160 pixels wide, grow to share any spare space, and shrink if you must. With flex-wrap: wrap, items that do not fit move to a new row. On a computer the three boxes sit in one row; on a narrow phone the third drops underneath — with no extra rules at all. list-style: none removes the bullets.

2.15 Styling the table

table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  padding: 8px 12px;
  text-align: left;
  border-bottom: 1px solid #dde2e8;
}

thead th {
  background: #1b2435;
  color: #ffffff;
}

tbody tr:nth-child(even) {
  background: #f4f6f8;
}

.num {
  text-align: right;
}
  • border-collapse: collapse merges the borders of neighbouring cells into single lines, instead of the browser’s default double lines.
  • Shading every second row — zebra stripes — helps the eye follow a row across a wide table.
  • Numbers line up best on the right, so that units sit under units and tens under tens. In the HTML, every number cell and its heading has class="num".

The grade in the last column gets a coloured badge. Every grade shares the grade class for its shape, and has a second class for its colour:

.grade {
  display: inline-block;
  min-width: 2em;
  text-align: center;
  border-radius: 4px;
  font-weight: bold;
}

.grade-a { background: #d4f0d2; color: #1d5e1a; }
.grade-b { background: #dbeafe; color: #1e4f8a; }

The full file also has grade-c, grade-d and grade-f. Notice that the letter is still in the HTML — <span class="grade grade-a">A</span>. Colour helps, but some readers cannot tell red from green, and a page printed in black and white loses colour altogether. Never let colour be the only way information is shown.

2.16 Making it work on a phone

Most of your visitors will open the page on a phone. Three things make it work.

The viewport tag. Without it, phones pretend to be a wide computer screen and shrink the whole page until it is too small to read. Add it to the head, under the charset line:

<meta name="viewport" content="width=device-width, initial-scale=1">

Tables that scroll. A table with eight columns does not fit across a phone. Wrap it in a div and let only the table scroll sideways, rather than the whole page:

<div class="table-wrap">
  <table>…</table>
</div>
.table-wrap {
  overflow-x: auto;
}

Media queries. A media query applies rules only when a condition is true — here, when the screen is 600 pixels wide or less:

@media (max-width: 600px) {
  .site-header {
    padding: 12px 16px;
  }

  main {
    padding: 16px;
  }

  section {
    padding: 16px;
  }
}

On a phone every pixel counts, so the padding shrinks. Because the media query comes after the normal rules, its values win the cascade.

2.17 Printing

Results get printed for noticeboards and parents’ meetings. Another media query changes the page for paper: no dark backgrounds that waste ink, and no form, since no one can type on paper.

@media print {
  body,
  .site-header {
    background: #ffffff;
    color: #000000;
  }

  .entry,
  footer {
    display: none;
  }
}

display: none removes an element completely. Press Ctrl + P in the browser to see the print preview: one clean page with the summary and the table. The full file in section 2.20 also removes the boxes around the sections and turns the table heading black on white.

2.18 The developer tools

Every modern browser has developer tools built in. Press F12, or right-click any part of the page and choose Inspect.

  • Elements shows your HTML as the browser understood it. Click an element to see every CSS rule that applies to it, in the Styles panel. A rule that lost the cascade is shown crossed out — that is how you find out why your style “does nothing”. You can change values right there to try ideas, but the changes disappear when you reload; copy the ones you like into style.css.
  • Console lists errors. If logo.png is missing, you will see a message that it was not found — the 404 from Chapter 1.
  • The device toolbar (Ctrl + Shift + M while the tools are open) shows the page at the size of a phone, so you can test section 2.16 without one.

2.19 When something goes wrong

What you seeMost likely cause
The page shows your code as textThe file is saved as .txt, not .html — turn on file extensions (Chapter 1)
No styles at allThe href in the link tag does not match the CSS file’s name or folder, or the link is outside the head
One rule has no effectA missing ; or } just above it, a misspelt property, or another rule winning the cascade — check Styles in the developer tools
A class does nothingThe dot is missing in the CSS (grade instead of .grade), or the spelling differs between HTML and CSS — class names are case-sensitive
The image shows only its alt textThe file name or folder in src is wrong — check spelling, capitals and the extension
Half the page is bold or inside a linkA closing tag is missing, such as </strong> or </a>
The table columns are out of lineA row has a cell too many or too few
Changes do not appearThe file is not saved, or the browser was not reloaded with Ctrl + R
Tiny text on a phoneThe viewport meta tag is missing

2.20 The complete files

Here are both files as they should be at the end of this chapter. Compare them with yours line by line if something does not look like the description, and keep them: Chapter 3 starts from exactly here.

web/results.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Form 4 Results, Term 1 2026 - Mwangaza Secondary School</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header">
    <img src="logo.png" alt="Mwangaza Secondary School badge" width="56" height="56">
    <div>
      <h1>Mwangaza Secondary School</h1>
      <p>P.O. Box 1234, Dodoma</p>
    </div>
  </header>

  <main>
    <section>
      <h2>Form 4 - Term 1 examination, 2026</h2>
      <p>Class teacher: <strong>Mwl. Grace Mushi</strong></p>
      <ul class="stats">
        <li><span>5</span> students</li>
        <li><span>66.3</span> class average</li>
        <li><span>Neema Kileo</span> top student</li>
      </ul>
    </section>

    <section>
      <h2>Results</h2>
      <div class="table-wrap">
        <table>
          <caption>Marks out of 100. Grades: A 75-100, B 65-74, C 45-64, D 30-44, F 0-29.</caption>
          <thead>
            <tr>
              <th scope="col">No.</th>
              <th scope="col">Student</th>
              <th scope="col">Sex</th>
              <th scope="col" class="num">Mathematics</th>
              <th scope="col" class="num">English</th>
              <th scope="col" class="num">Biology</th>
              <th scope="col" class="num">Average</th>
              <th scope="col">Grade</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>1</td>
              <td>Amina Juma</td>
              <td>F</td>
              <td class="num">78</td>
              <td class="num">85</td>
              <td class="num">81</td>
              <td class="num">81.3</td>
              <td><span class="grade grade-a">A</span></td>
            </tr>
            <tr>
              <td>2</td>
              <td>Baraka Mushi</td>
              <td>M</td>
              <td class="num">52</td>
              <td class="num">61</td>
              <td class="num">47</td>
              <td class="num">53.3</td>
              <td><span class="grade grade-c">C</span></td>
            </tr>
            <tr>
              <td>3</td>
              <td>Neema Kileo</td>
              <td>F</td>
              <td class="num">91</td>
              <td class="num">88</td>
              <td class="num">94</td>
              <td class="num">91.0</td>
              <td><span class="grade grade-a">A</span></td>
            </tr>
            <tr>
              <td>4</td>
              <td>Juma Hassan</td>
              <td>M</td>
              <td class="num">34</td>
              <td class="num">49</td>
              <td class="num">38</td>
              <td class="num">40.3</td>
              <td><span class="grade grade-d">D</span></td>
            </tr>
            <tr>
              <td>5</td>
              <td>Rehema Mollel</td>
              <td>F</td>
              <td class="num">66</td>
              <td class="num">72</td>
              <td class="num">59</td>
              <td class="num">65.7</td>
              <td><span class="grade grade-b">B</span></td>
            </tr>
          </tbody>
        </table>
      </div>
    </section>

    <section class="entry">
      <h2>Enter a mark</h2>
      <form>
        <label for="student">Student</label>
        <select id="student" name="student">
          <option>Amina Juma</option>
          <option>Baraka Mushi</option>
          <option>Neema Kileo</option>
          <option>Juma Hassan</option>
          <option>Rehema Mollel</option>
        </select>

        <label for="subject">Subject</label>
        <select id="subject" name="subject">
          <option>Mathematics</option>
          <option>English</option>
          <option>Biology</option>
        </select>

        <label for="mark">Mark (0 to 100)</label>
        <input id="mark" name="mark" type="number" min="0" max="100" required>

        <button type="submit">Save mark</button>
      </form>
      <p class="note">The form checks the mark is between 0 and 100. Chapter 3 makes Save work.</p>
    </section>
  </main>

  <footer>
    <p>Mwangaza Secondary School results system. National results: <a href="https://www.necta.go.tz">necta.go.tz</a></p>
  </footer>
</body>
</html>

web/style.css

/* Mwangaza Secondary School - results page */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: "Segoe UI", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  color: #1b2435;
  background: #f4f6f8;
}

/* Header */
.site-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 24px;
  background: #1b2435;
  color: #ffffff;
}

.site-header h1 {
  margin: 0;
  font-size: 1.5rem;
}

.site-header p {
  margin: 0;
  color: #c9d1dc;
}

/* Page content */
main {
  max-width: 960px;
  margin: 0 auto;
  padding: 24px;
}

section {
  background: #ffffff;
  border: 1px solid #dde2e8;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 24px;
}

h2 {
  margin-top: 0;
  font-size: 1.25rem;
}

/* The three numbers at the top */
.stats {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stats li {
  flex: 1 1 160px;
  padding: 12px;
  background: #eef8ee;
  border-radius: 6px;
}

.stats span {
  display: block;
  font-size: 1.5rem;
  font-weight: bold;
  color: #2e8b2a;
}

/* Results table */
.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

caption {
  text-align: left;
  color: #4a5568;
  padding-bottom: 8px;
}

th,
td {
  padding: 8px 12px;
  text-align: left;
  border-bottom: 1px solid #dde2e8;
}

thead th {
  background: #1b2435;
  color: #ffffff;
}

tbody tr:nth-child(even) {
  background: #f4f6f8;
}

.num {
  text-align: right;
}

.grade {
  display: inline-block;
  min-width: 2em;
  text-align: center;
  border-radius: 4px;
  font-weight: bold;
}

.grade-a { background: #d4f0d2; color: #1d5e1a; }
.grade-b { background: #dbeafe; color: #1e4f8a; }
.grade-c { background: #fef3c7; color: #7a5200; }
.grade-d { background: #fde2cf; color: #8a3b12; }
.grade-f { background: #fde0e0; color: #9b1c1c; }

/* Mark entry form */
form {
  display: grid;
  gap: 8px;
  max-width: 320px;
}

label {
  font-weight: 600;
}

input,
select,
button {
  font: inherit;
  padding: 8px;
  border: 1px solid #b8c1cc;
  border-radius: 6px;
}

button {
  background: #2e8b2a;
  color: #ffffff;
  border: none;
  cursor: pointer;
}

button:hover {
  background: #246f21;
}

.note {
  color: #4a5568;
  font-size: 0.9rem;
}

footer {
  text-align: center;
  padding: 16px;
  color: #4a5568;
  font-size: 0.9rem;
}

/* Phones */
@media (max-width: 600px) {
  .site-header {
    padding: 12px 16px;
  }

  main {
    padding: 16px;
  }

  section {
    padding: 16px;
  }
}

/* Paper */
@media print {
  body,
  .site-header {
    background: #ffffff;
    color: #000000;
  }

  .site-header p {
    color: #000000;
  }

  .entry,
  footer {
    display: none;
  }

  section {
    border: none;
    padding: 0;
  }

  thead th {
    background: #ffffff;
    color: #000000;
    border-bottom: 2px solid #000000;
  }
}

Try it yourself

  1. Add a fourth subject, Kiswahili, to the table: a heading cell and one mark in every row. Recalculate each average by hand.
  2. Add a sixth student. Remember to give their number, sex, marks, average and grade.
  3. Change the green of the .stats numbers to your school’s colour, and check that it is still easy to read.
  4. Use the device toolbar in the developer tools to view the page at the size of a small phone. Then print it to PDF with Ctrl + P.

Key terms

EnglishKiswahiliMeaning
Web pageUkurasa wa wavutiOne document on a website, written in HTML
Code editorKihariri msimboA program for writing code, such as VS Code
ElementKipengeleOne part of an HTML page, such as a heading or a table
AttributeSifaExtra information inside an opening tag, such as href
HeadingKichwaA title for a page or a section
LinkKiungoText or an image that opens another page when clicked
ImagePichaA picture shown on the page
TableJedwaliData arranged in rows and columns
FormFomuThe part of a page where visitors type or choose information
StylesheetLaha ya mitindoA CSS file that controls how pages look
ColourRangiWritten in CSS as a hex code such as #1b2435
LayoutMpangilioHow the parts of a page are arranged

Check your understanding

  1. Which file — the HTML or the CSS — decides that “Mwangaza Secondary School” is the main heading, and which decides that it is white?
  2. Find the mistake: <p>Class teacher: <strong>Mwl. Grace Mushi</p></strong>.
  3. Why should every image have an alt attribute? Give two reasons.
  4. A label has for="mark". What must the matching input have, and what does the connection do for the visitor?
  5. What is the difference between .grade and #grade in CSS?
  6. A section has padding: 20px, border: 1px solid and margin-bottom: 24px. Which of these takes the section’s background colour?
  7. The page looks perfect on a computer, but on a phone everything is tiny. What is missing?
  8. Why does the grade column show a letter as well as a colour?
  9. You changed a colour in style.css, saved and reloaded, but nothing changed. List three things to check.

Answers

  1. The HTML decides it is the main heading (<h1>). The CSS decides it is white (color: #ffffff on .site-header).
  2. The tags close in the wrong order. <strong> was opened last, so it must close first: <p>Class teacher: <strong>Mwl. Grace Mushi</strong></p>.
  3. Screen readers read it aloud to blind visitors, and the browser shows it when the image cannot load. Search engines also use it to understand the image.
  4. The input must have id="mark". Clicking the label then puts the cursor in the box, and screen readers announce the label when the box is reached.
  5. .grade chooses every element with class="grade". #grade chooses the single element with id="grade".
  6. The padding. The margin is outside the border and shows whatever is behind the section.
  7. The viewport tag: <meta name="viewport" content="width=device-width, initial-scale=1">.
  8. Some readers cannot tell certain colours apart, and a black-and-white printout loses colour completely. The letter carries the meaning; the colour only helps.
  9. For example: that the CSS file is linked correctly in the head; that there is no missing ; or } above the rule; that another rule is not winning the cascade (check Styles in the developer tools); and that the right file was saved.
Chapter 3

JavaScript

Check marks as they are entered, and work out averages, grades and positions.

At the end of Chapter 2 the results page looked finished, but it could not do anything. Every average was worked out by hand, the Save mark button only reloaded the page, and if one mark changed, someone had to recalculate the average, the grade and the class average with a calculator. In this chapter you add the third language of the web, JavaScript, and the page starts to work: it calculates averages, grades and positions by itself, checks each mark as it is typed, saves new marks, and remembers them the next time it is opened.

By the end of this chapter you will be able to:

  • add a JavaScript file to a page and try code in the browser’s console;
  • store values in variables, and do arithmetic and rounding correctly;
  • make decisions with if and else;
  • write your own functions and reuse them;
  • keep a class list in arrays and objects, and loop through it;
  • read and change a page from JavaScript, safely;
  • respond to typing and clicking with events;
  • save data in the browser so it is still there tomorrow;
  • read error messages and find your own mistakes.

You will start from the two files you finished in Chapter 2, results.html and style.css, in your web folder.

3.1 What JavaScript does

You now know three languages that work together, each with its own job:

LanguageJobOn our page
HTMLWhat is on the pageA table, a form, a button
CSSHow it looksNavy heading row, green grade badges
JavaScriptWhat it doesWorks out averages, checks marks, saves them

JavaScript is a full programming language. It can store information, do calculations, make decisions and repeat work — and every web browser can run it. That makes it the only programming language you can use on a web page without installing anything.

Adding a script to the page

Create a new file in the web folder called results.js. Then add one line to the head of results.html, under the link to the stylesheet:

<script src="results.js" defer></script>

The defer attribute matters. Without it, the browser would run the script the moment it reached this line in the head — before the table and the form below it exist — and the script would not find them. With defer, the browser downloads the script straight away but waits to run it until the whole page has been read.

3.2 The console

Before you write the file, try JavaScript one line at a time. Open the page, press F12 and choose the Console tab. Type each line below and press Enter:

2 + 3
(78 + 85 + 81) / 3
"Habari, " + "Amina"
console.log("The page is working")

The console answers each line straight away: 5, 81.33333333333333, "Habari, Amina". console.log prints a message; you will use it constantly to check what your program is doing. Anything a script prints with console.log appears here, and so does every error.

3.3 Values and variables

Programs work with values. The three kinds you need first are:

KindExamplesUsed for
Number78, 81.3, -5Marks, averages, counts
String (text)"Amina Juma", "Biology"Names, subjects, messages — always in quotes
Booleantrue, falseThe answer to a yes-or-no question

A variable is a name for a value, so you can use it later. Create one with const or let:

const subject = "Mathematics";
let total = 0;
total = total + 78;
  • const means the name will always refer to the same value. Use it whenever you can.
  • let means the value may change later, like a running total.
  • = puts the value on the right into the name on the left. As Chapter 1 warned, it does not mean “is equal to”.

Names cannot contain spaces. JavaScript programmers write longer names in camelCase — every word after the first starts with a capital letter: classAverage, topStudent, markInput. Choose names that say what the value is; averageOfAmina is clear, x2 is not.

3.4 Arithmetic and rounding

JavaScript uses the symbols from Chapter 1: +, -, * for multiply, / for divide and % for the remainder. Multiplication and division happen before addition and subtraction, just as in mathematics, so use brackets to say what you mean:

78 + 85 + 81 / 3     // 189 — only 81 is divided
(78 + 85 + 81) / 3   // 81.33333333333333

Everything after // on a line is a comment: the computer ignores it.

Rounding

An average such as 81.33333333333333 needs rounding to one decimal place. Math.round rounds to a whole number, so we multiply by 10 first and divide afterwards:

function roundTo1(number) {
  return Math.round(number * 10) / 10;
}

roundTo1(81.333) gives 81.3. To show a number with exactly one decimal place, use toFixed(1): (91).toFixed(1) gives "91.0" — notice that the answer is a string, ready to display, not a number to calculate with.

The string trap

"78" + 5          // "785"
Number("78") + 5  // 83

When one side of + is a string, JavaScript joins instead of adding. Everything a visitor types into a form reaches your script as a string — even in a box with type="number" — so turn it into a number with Number() before you calculate. If the text is not a number at all, Number gives the special value NaN, “not a number”, which you will meet again in section 3.13.

3.5 Making decisions

A grade depends on the mark. In code, a decision is an if statement:

function gradeFor(mark) {
  if (mark >= 75) {
    return "A";
  } else if (mark >= 65) {
    return "B";
  } else if (mark >= 45) {
    return "C";
  } else if (mark >= 30) {
    return "D";
  } else {
    return "F";
  }
}

The computer checks each condition from the top and stops at the first one that is true. For a mark of 70, 70 >= 75 is false, so it moves on; 70 >= 65 is true, so the answer is "B" and the remaining lines are skipped. The order matters: if the test for D came first, every mark of 30 or more would get a D.

Always test the edges, where mistakes hide: gradeFor(75) must give "A" and gradeFor(74) must give "B". Try them in the console once your file is loaded.

OperatorMeansExample that is true
===Is equal tograde === "A"
!==Is not equal tograde !== "F"
>, <Greater than, less than81 > 75
>=, <=Greater than or equal to, less than or equal to75 >= 75
&&And — both must be truemark >= 0 && mark <= 100
||Or — at least one must be truemark < 0 || mark > 100
!Not — turns true into false!false

3.6 Functions

gradeFor and roundTo1 are functions: named pieces of code that you can use again and again. A function has:

  • a name, such as gradeFor;
  • parameters in brackets — the values it needs to do its job, such as mark;
  • a body in curly braces — the steps;
  • a return value — the answer it gives back, after return.

Defining a function does nothing on its own. It runs when you call it, by writing its name followed by the values in brackets: gradeFor(81.3) gives "A".

Functions are why programs stay manageable. The grade boundaries live in exactly one place. If they change, you change gradeFor, and every grade on the page — in the table, in a report, anywhere — follows.

3.7 Arrays and objects

A whole class needs more than single variables. JavaScript has two ways to group values.

An array is a list in square brackets. Items are counted from zero:

const SUBJECTS = ["Mathematics", "English", "Biology"];
SUBJECTS[0]       // "Mathematics"
SUBJECTS[2]       // "Biology"
SUBJECTS.length   // 3

An object groups named values, called properties, in curly braces:

const student = { name: "Amina Juma", sex: "F", marks: { Mathematics: 78, English: 85, Biology: 81 } };
student.name               // "Amina Juma"
student.marks.Biology      // 81
student.marks["Biology"]   // 81 — the same, with the name as a string

The square-bracket form is useful when the property name is in a variable: if subject holds "Biology", then student.marks[subject] is 81.

Put the two together — an array of objects — and you have a class list. This is the start of results.js:

const SUBJECTS = ["Mathematics", "English", "Biology"];
const STORAGE_KEY = "mwangaza-form4-term1-2026";

// The marks from Chapter 2, now kept as data instead of typed into the table.
const startingStudents = [
  { name: "Amina Juma", sex: "F", marks: { Mathematics: 78, English: 85, Biology: 81 } },
  { name: "Baraka Mushi", sex: "M", marks: { Mathematics: 52, English: 61, Biology: 47 } },
  { name: "Neema Kileo", sex: "F", marks: { Mathematics: 91, English: 88, Biology: 94 } },
  { name: "Juma Hassan", sex: "M", marks: { Mathematics: 34, English: 49, Biology: 38 } },
  { name: "Rehema Mollel", sex: "F", marks: { Mathematics: 66, English: 72, Biology: 59 } },
];

Names written in capitals, such as SUBJECTS, are a convention for settings that never change while the program runs. STORAGE_KEY is used in section 3.11.

The big idea of this chapter starts here: the data is the truth, and the page is drawn from it. In Chapter 2 the marks lived inside the HTML table. Now they live in this array, and the table will be built from it. Change a mark in the data, redraw the table, and every average, grade and position is right.

3.8 Loops

A loop repeats work for every item in a list. for...of takes the items one at a time:

function averageOf(student) {
  let total = 0;
  for (const subject of SUBJECTS) {
    total = total + student.marks[subject];
  }
  return roundTo1(total / SUBJECTS.length);
}

The first time round, subject is "Mathematics"; then "English"; then "Biology". Each time, that subject’s mark is added to total. Dividing by SUBJECTS.length rather than by 3 means the function still works when you add a fourth subject.

When you need the position in the list as well, use the counting form of for:

for (let i = 0; i < students.length; i++) {
  const student = students[i];
  // ...
}

Read it as: start with i at 0; keep going while i is less than the number of students; after each round, add one to i (i++). For five students, i is 0, 1, 2, 3 and 4.

Positions

Schools rank students by average, and students with the same average share a position. If two students tie for second place, both are 2nd and the next student is 4th — no one is 3rd. There is a neat way to work this out: a student’s position is one more than the number of students with a higher average.

// Position 1 has the highest average. Students with the same average share
// a position, and the next position is skipped: 1, 2, 2, 4.
function positionOf(student) {
  let higher = 0;
  for (const other of students) {
    if (averageOf(other) > averageOf(student)) {
      higher = higher + 1;
    }
  }
  return higher + 1;
}

Nobody has a higher average than Neema, so she is 1st. Only Neema is above Amina, so Amina is 2nd. Two students with the same average have the same number of students above them, so they share a position automatically.

3.9 Changing the page

The browser turns your HTML into a tree of objects called the DOM (Document Object Model). JavaScript can find any element in it, read it and change it.

Finding elements

document.querySelector takes a CSS selector — exactly the kind you wrote in Chapter 2 — and gives back the first element that matches:

const tableBody = document.querySelector("#results-body");

For that to work, the elements need ids. In results.html, the body of the table loses its five typed rows and becomes empty, ready for JavaScript to fill:

<tbody id="results-body"></tbody>

The three summary numbers get ids too, with a dash showing until the script fills them in:

<ul class="stats">
  <li><span id="count">-</span> students</li>
  <li><span id="class-average">-</span> class average</li>
  <li><span id="top-student">-</span> top student</li>
</ul>

Add a ninth heading to the table, after Grade, for the new column:

<th scope="col" class="num">Position</th>

Building the table

To make a new element, create it, fill it, and attach it to the page:

function addCell(row, text, className) {
  const cell = document.createElement("td");
  cell.textContent = text;
  if (className) {
    cell.className = className;
  }
  row.appendChild(cell);
  return cell;
}

createElement("td") makes a new, empty cell that is not yet on the page. textContent sets the text inside it, className sets its class attribute, and appendChild puts it inside the row. A parameter that is not given is undefined, so if (className) skips that step when a cell needs no class.

Now a function can build every row from the data:

function renderTable() {
  tableBody.textContent = "";

  for (let i = 0; i < students.length; i++) {
    const student = students[i];
    const average = averageOf(student);
    const grade = gradeFor(average);
    const row = document.createElement("tr");

    addCell(row, i + 1);
    addCell(row, student.name);
    addCell(row, student.sex);
    for (const subject of SUBJECTS) {
      addCell(row, student.marks[subject], "num");
    }
    addCell(row, average.toFixed(1), "num");

    const badge = document.createElement("span");
    badge.className = "grade grade-" + grade.toLowerCase();
    badge.textContent = grade;
    addCell(row, "").appendChild(badge);

    addCell(row, positionOf(student), "num");
    tableBody.appendChild(row);
  }
}

Setting tableBody.textContent = "" first removes the old rows, so the function can be called again and again — after every change — without the table growing. The grade badge is built from the grade itself: "grade grade-" + "a" gives the class grade grade-a, and the CSS from Chapter 2 colours it.

The summary

function renderSummary() {
  let total = 0;
  let top = students[0];
  for (const student of students) {
    total = total + averageOf(student);
    if (averageOf(student) > averageOf(top)) {
      top = student;
    }
  }

  document.querySelector("#count").textContent = students.length;
  document.querySelector("#class-average").textContent = (total / students.length).toFixed(1);
  document.querySelector("#top-student").textContent = top.name;
}

function render() {
  renderTable();
  renderSummary();
}

The loop does two jobs at once: it adds up the averages for the class average, and it keeps track of the best student seen so far. It starts by assuming the first student is the best, and replaces them whenever it meets a higher average.

The last two lines of results.js start everything when the page opens:

fillChoices();
render();

fillChoices is in the next section. Reload the page: the table is back, drawn by your code, with a Position column — and the averages match the ones you worked out by hand in Chapter 2.

3.10 Events: responding to the teacher

An event is something that happens on the page: a click, a key press, a form being sent. addEventListener tells the browser to run a function whenever a particular event happens to a particular element.

Filling the choices

First, the form. Its two drop-down lists lose their typed option elements, and the form gets an id and one new attribute:

<form id="mark-form" novalidate>
  <label for="student">Student</label>
  <select id="student" name="student"></select>

  <label for="subject">Subject</label>
  <select id="subject" name="subject"></select>

  <label for="mark">Mark (0 to 100)</label>
  <input id="mark" name="mark" type="number" min="0" max="100" required>

  <button type="submit">Save mark</button>
  <button type="button" id="reset" class="secondary">Restore original marks</button>
</form>
<p id="message" class="message" role="status"></p>

novalidate switches off the browser’s own checking, because the script will check marks itself with clearer messages. The p with role="status" is where messages appear; the role makes screen readers announce them. The second button has type="button", so clicking it does not send the form.

The script fills the drop-down lists from the data, so they can never disagree with the table:

function fillChoices() {
  for (let i = 0; i < students.length; i++) {
    const option = document.createElement("option");
    option.value = i;
    option.textContent = students[i].name;
    studentSelect.appendChild(option);
  }
  for (const subject of SUBJECTS) {
    const option = document.createElement("option");
    option.textContent = subject;
    subjectSelect.appendChild(option);
  }
}

Each student’s option has the student’s number in the list as its value, so when the form is sent, students[Number(studentSelect.value)] is the right student.

Checking a mark

One function decides whether a mark is acceptable, and returns a message saying what is wrong — or an empty string if nothing is:

// Returns what is wrong with a typed mark, or "" if it is fine.
function checkMark(text) {
  if (text.trim() === "") {
    return "Type a mark.";
  }
  const mark = Number(text);
  if (!Number.isInteger(mark)) {
    return "Marks are whole numbers, such as 67.";
  }
  if (mark < 0 || mark > 100) {
    return "A mark must be between 0 and 100.";
  }
  return "";
}

trim() removes spaces from both ends, so a box containing only spaces counts as empty. Number.isInteger is true only for whole numbers, so 67.5 — and NaN — are refused.

The input event happens every time the content of a box changes, so the teacher sees a problem while typing, not after:

function showMessage(text, kind) {
  message.textContent = text;
  message.className = "message " + kind;
}

markInput.addEventListener("input", function () {
  showMessage(checkMark(markInput.value), "error");
});

The function given to addEventListener has no name; it exists only to be run when the event happens. When the mark is fine, checkMark returns "" and the message disappears.

Saving a mark

form.addEventListener("submit", function (event) {
  event.preventDefault();

  const problem = checkMark(markInput.value);
  if (problem !== "") {
    showMessage(problem, "error");
    markInput.focus();
    return;
  }

  const student = students[Number(studentSelect.value)];
  const subject = subjectSelect.value;
  const mark = Number(markInput.value);

  student.marks[subject] = mark;
  saveStudents();
  render();

  showMessage(`Saved: ${student.name}, ${subject} ${mark}.`, "success");
  markInput.value = "";
});

Step by step:

  1. event.preventDefault() stops the browser’s normal behaviour for a form, which is to reload the page.
  2. The mark is checked again. If there is a problem, the message is shown, the cursor goes back to the box, and return leaves the function early — nothing is saved.
  3. The new mark goes into the data: student.marks[subject] = mark.
  4. The data is saved (next section), and render() redraws the table and the summary from it.
  5. A confirmation appears. The text between backticks is a template string: everything inside ${ } is replaced by its value, giving, for example, “Saved: Juma Hassan, Biology 58.”

Try it: give Juma Hassan 58 in Biology. His average rises from 40.3 to 47.0, his grade from D to C, and the class average changes — all without a calculator.

The variables used by these functions are found once, when the script starts:

const form = document.querySelector("#mark-form");
const studentSelect = document.querySelector("#student");
const subjectSelect = document.querySelector("#subject");
const markInput = document.querySelector("#mark");
const message = document.querySelector("#message");
const resetButton = document.querySelector("#reset");

Add the styles for the messages and the second button to the end of style.css:

/* Chapter 3: messages and the second button */
.message {
  min-height: 1.5em;
  margin: 12px 0 0;
  font-weight: 600;
}

.message.error {
  color: #9b1c1c;
}

.message.success {
  color: #1d5e1a;
}

button.secondary {
  background: #ffffff;
  color: #1b2435;
  border: 1px solid #b8c1cc;
}

button.secondary:hover {
  background: #f4f6f8;
}

.message.error, with no space, means an element that has both classes. min-height keeps space for one line of message, so the page does not jump when a message appears.

3.11 Remembering marks

Enter a mark, then reload the page: the mark is gone. The students array lives in the browser’s memory — the “desk” from Chapter 1 — and a reload clears the desk. The browser has a small “cupboard” each website can use, called localStorage. It keeps text, under a name you choose, until it is deleted.

localStorage can only store strings, so the array is turned into text with JSON.stringify and back again with JSON.parse. JSON is a way of writing data as text that looks almost exactly like JavaScript arrays and objects.

function loadStudents() {
  const saved = localStorage.getItem(STORAGE_KEY);
  if (saved !== null) {
    try {
      return JSON.parse(saved);
    } catch (error) {
      console.warn("The saved marks could not be read, so the originals are used.", error);
    }
  }
  return structuredClone(startingStudents);
}

function saveStudents() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(students));
}
  • getItem gives back the saved text, or null if nothing has been saved yet.
  • try and catch handle a problem without stopping the whole program. If the saved text is damaged, JSON.parse throws an error; instead of the page breaking, the catch part runs, a warning goes to the console, and the original marks are used.
  • structuredClone makes a complete, separate copy. Changing the copy never changes startingStudents, so the originals are always there to go back to.

Near the top of the file, the class list is loaded once, when the page opens:

let students = loadStudents();

It is let, not const, because the restore button replaces it with a fresh copy.

Putting the originals back

resetButton.addEventListener("click", function () {
  if (confirm("Put back the original marks? Marks you entered will be lost.")) {
    localStorage.removeItem(STORAGE_KEY);
    students = structuredClone(startingStudents);
    render();
    showMessage("The original marks are back.", "success");
  }
});

confirm shows a box with OK and Cancel, and gives back true only if the teacher chose OK. Always ask before throwing work away.

3.12 The whole program, in order

results.js is now complete. Read the full listing in section 3.14 from top to bottom and notice its shape, which most small programs share:

  1. Settings and data: SUBJECTS, STORAGE_KEY, startingStudents, and students loaded from storage.
  2. Calculations: gradeFor, roundTo1, averageOf, positionOf, checkMark. They only work things out; none of them touches the page.
  3. Saving: loadStudents, saveStudents.
  4. Showing: addCell, renderTable, renderSummary, render.
  5. Responding: the form, the buttons and their events.
  6. Start: fillChoices() and render().

Keeping calculations apart from the page is deliberate. In Chapter 5 you will rewrite gradeFor and averageOf in Python, and because they never touched the page, they move across almost unchanged.

Divisions — Division I to IV and Division 0 — are worked out from a student’s best seven subjects. With three subjects the page cannot do it yet; Chapter 5 adds it when the class has a full set of subjects.

3.13 Finding mistakes

Every programmer makes mistakes every day. The skill is finding them quickly. When the page does not do what you expect, open the console (F12) first. A red error tells you what went wrong and, on the right, which file and line.

  • Uncaught ReferenceError: totl is not defined
    A name is misspelt, or used before it was created. Names are case-sensitive: markinput is not markInput.
  • Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
    querySelector found nothing: the id in the script does not match the HTML, or the script ran before the page existed — check for defer.
  • Uncaught SyntaxError: Unexpected token '}'
    A bracket, brace or quote is missing or extra near that line. VS Code highlights matching pairs when you click next to one.
  • NaN appears in the table
    A calculation used something that is not a number — often a subject that a student has no mark for.
  • 785 where you expected 83
    A string was joined to a number. Use Number() on typed values.
  • Nothing happens and there is no error
    The script is not linked, the file was not saved, or the page was not reloaded.
  • Uncaught TypeError: render is not a function
    A variable has the same name as a function, or the function name is misspelt.

When there is no error but the answer is wrong, make the program tell you what it is doing. Add console.log lines, reload, and read the console:

console.log("average of", student.name, "is", average);

Remove them when the problem is fixed. And use the console to test functions directly: gradeFor(74), averageOf(students[0]), positionOf(students[3]).

3.14 The complete files

These are the files at the end of the chapter. style.css is the Chapter 2 file with the rules from section 3.10 added to the end.

web/results.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Form 4 Results, Term 1 2026 - Mwangaza Secondary School</title>
  <link rel="stylesheet" href="style.css">
  <script src="results.js" defer></script>
</head>
<body>
  <header class="site-header">
    <img src="logo.png" alt="Mwangaza Secondary School badge" width="56" height="56">
    <div>
      <h1>Mwangaza Secondary School</h1>
      <p>P.O. Box 1234, Dodoma</p>
    </div>
  </header>

  <main>
    <section>
      <h2>Form 4 - Term 1 examination, 2026</h2>
      <p>Class teacher: <strong>Mwl. Grace Mushi</strong></p>
      <ul class="stats">
        <li><span id="count">-</span> students</li>
        <li><span id="class-average">-</span> class average</li>
        <li><span id="top-student">-</span> top student</li>
      </ul>
    </section>

    <section>
      <h2>Results</h2>
      <div class="table-wrap">
        <table>
          <caption>Marks out of 100. Grades: A 75-100, B 65-74, C 45-64, D 30-44, F 0-29.</caption>
          <thead>
            <tr>
              <th scope="col">No.</th>
              <th scope="col">Student</th>
              <th scope="col">Sex</th>
              <th scope="col" class="num">Mathematics</th>
              <th scope="col" class="num">English</th>
              <th scope="col" class="num">Biology</th>
              <th scope="col" class="num">Average</th>
              <th scope="col">Grade</th>
              <th scope="col" class="num">Position</th>
            </tr>
          </thead>
          <tbody id="results-body"></tbody>
        </table>
      </div>
    </section>

    <section class="entry">
      <h2>Enter a mark</h2>
      <form id="mark-form" novalidate>
        <label for="student">Student</label>
        <select id="student" name="student"></select>

        <label for="subject">Subject</label>
        <select id="subject" name="subject"></select>

        <label for="mark">Mark (0 to 100)</label>
        <input id="mark" name="mark" type="number" min="0" max="100" required>

        <button type="submit">Save mark</button>
        <button type="button" id="reset" class="secondary">Restore original marks</button>
      </form>
      <p id="message" class="message" role="status"></p>
    </section>
  </main>

  <footer>
    <p>Mwangaza Secondary School results system. National results: <a href="https://www.necta.go.tz">necta.go.tz</a></p>
  </footer>
</body>
</html>

web/results.js

// Mwangaza Secondary School - results page
// Works out averages, grades and positions, and lets a teacher enter marks.

const SUBJECTS = ["Mathematics", "English", "Biology"];
const STORAGE_KEY = "mwangaza-form4-term1-2026";

// The marks from Chapter 2, now kept as data instead of typed into the table.
const startingStudents = [
  { name: "Amina Juma", sex: "F", marks: { Mathematics: 78, English: 85, Biology: 81 } },
  { name: "Baraka Mushi", sex: "M", marks: { Mathematics: 52, English: 61, Biology: 47 } },
  { name: "Neema Kileo", sex: "F", marks: { Mathematics: 91, English: 88, Biology: 94 } },
  { name: "Juma Hassan", sex: "M", marks: { Mathematics: 34, English: 49, Biology: 38 } },
  { name: "Rehema Mollel", sex: "F", marks: { Mathematics: 66, English: 72, Biology: 59 } },
];

let students = loadStudents();

// ---- Calculations ----

function gradeFor(mark) {
  if (mark >= 75) {
    return "A";
  } else if (mark >= 65) {
    return "B";
  } else if (mark >= 45) {
    return "C";
  } else if (mark >= 30) {
    return "D";
  } else {
    return "F";
  }
}

function roundTo1(number) {
  return Math.round(number * 10) / 10;
}

function averageOf(student) {
  let total = 0;
  for (const subject of SUBJECTS) {
    total = total + student.marks[subject];
  }
  return roundTo1(total / SUBJECTS.length);
}

// Position 1 has the highest average. Students with the same average share
// a position, and the next position is skipped: 1, 2, 2, 4.
function positionOf(student) {
  let higher = 0;
  for (const other of students) {
    if (averageOf(other) > averageOf(student)) {
      higher = higher + 1;
    }
  }
  return higher + 1;
}

// Returns what is wrong with a typed mark, or "" if it is fine.
function checkMark(text) {
  if (text.trim() === "") {
    return "Type a mark.";
  }
  const mark = Number(text);
  if (!Number.isInteger(mark)) {
    return "Marks are whole numbers, such as 67.";
  }
  if (mark < 0 || mark > 100) {
    return "A mark must be between 0 and 100.";
  }
  return "";
}

// ---- Saving in the browser ----

function loadStudents() {
  const saved = localStorage.getItem(STORAGE_KEY);
  if (saved !== null) {
    try {
      return JSON.parse(saved);
    } catch (error) {
      console.warn("The saved marks could not be read, so the originals are used.", error);
    }
  }
  return structuredClone(startingStudents);
}

function saveStudents() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(students));
}

// ---- Showing the results ----

const tableBody = document.querySelector("#results-body");

function addCell(row, text, className) {
  const cell = document.createElement("td");
  cell.textContent = text;
  if (className) {
    cell.className = className;
  }
  row.appendChild(cell);
  return cell;
}

function renderTable() {
  tableBody.textContent = "";

  for (let i = 0; i < students.length; i++) {
    const student = students[i];
    const average = averageOf(student);
    const grade = gradeFor(average);
    const row = document.createElement("tr");

    addCell(row, i + 1);
    addCell(row, student.name);
    addCell(row, student.sex);
    for (const subject of SUBJECTS) {
      addCell(row, student.marks[subject], "num");
    }
    addCell(row, average.toFixed(1), "num");

    const badge = document.createElement("span");
    badge.className = "grade grade-" + grade.toLowerCase();
    badge.textContent = grade;
    addCell(row, "").appendChild(badge);

    addCell(row, positionOf(student), "num");
    tableBody.appendChild(row);
  }
}

function renderSummary() {
  let total = 0;
  let top = students[0];
  for (const student of students) {
    total = total + averageOf(student);
    if (averageOf(student) > averageOf(top)) {
      top = student;
    }
  }

  document.querySelector("#count").textContent = students.length;
  document.querySelector("#class-average").textContent = (total / students.length).toFixed(1);
  document.querySelector("#top-student").textContent = top.name;
}

function render() {
  renderTable();
  renderSummary();
}

// ---- Entering marks ----

const form = document.querySelector("#mark-form");
const studentSelect = document.querySelector("#student");
const subjectSelect = document.querySelector("#subject");
const markInput = document.querySelector("#mark");
const message = document.querySelector("#message");
const resetButton = document.querySelector("#reset");

// Fill the drop-down lists from the data, so they always match the table.
function fillChoices() {
  for (let i = 0; i < students.length; i++) {
    const option = document.createElement("option");
    option.value = i;
    option.textContent = students[i].name;
    studentSelect.appendChild(option);
  }
  for (const subject of SUBJECTS) {
    const option = document.createElement("option");
    option.textContent = subject;
    subjectSelect.appendChild(option);
  }
}

function showMessage(text, kind) {
  message.textContent = text;
  message.className = "message " + kind;
}

markInput.addEventListener("input", function () {
  showMessage(checkMark(markInput.value), "error");
});

form.addEventListener("submit", function (event) {
  event.preventDefault();

  const problem = checkMark(markInput.value);
  if (problem !== "") {
    showMessage(problem, "error");
    markInput.focus();
    return;
  }

  const student = students[Number(studentSelect.value)];
  const subject = subjectSelect.value;
  const mark = Number(markInput.value);

  student.marks[subject] = mark;
  saveStudents();
  render();

  showMessage(`Saved: ${student.name}, ${subject} ${mark}.`, "success");
  markInput.value = "";
});

resetButton.addEventListener("click", function () {
  if (confirm("Put back the original marks? Marks you entered will be lost.")) {
    localStorage.removeItem(STORAGE_KEY);
    students = structuredClone(startingStudents);
    render();
    showMessage("The original marks are back.", "success");
  }
});

// ---- Start ----

fillChoices();
render();

Try it yourself

  1. Add Kiswahili as a fourth subject: add it to SUBJECTS, give every student a Kiswahili mark in startingStudents, and add a heading cell to the table. Reload. If you see NaN, the page loaded the marks saved before Kiswahili existed — click Restore original marks. Explain why that fixes it.
  2. Add a fourth number to the summary: how many students have an average of 45 or more.
  3. Change checkMark so that a mark of exactly 0 asks “Was the student absent?” — while still allowing it to be saved.
  4. Make the Position column show “1st”, “2nd”, “3rd” and “4th” instead of plain numbers. Hint: write a function that takes a number and returns a string.

Key terms

EnglishKiswahiliMeaning
Programming languageLugha ya programuA language for giving a computer step-by-step instructions
VariableKigezoA name that holds a value
ValueThamaniA piece of data, such as a number or some text
StringMfuatano wa herufiText, written between quotes
FunctionKitendakaziA named set of steps that can be used again
ConditionShartiA question that is either true or false
LoopKitanziCode that repeats for each item or until something is true
ArraySafuAn ordered list of values
ObjectKituA group of named values
EventTukioSomething that happens on the page, such as a click
ErrorHitilafuA problem that stops code from running as intended
DebuggingKutatua hitilafuFinding and fixing mistakes in code

Check your understanding

  1. Why does the script tag need defer when it is placed in the head?
  2. What is the difference between const and let? Why is students declared with let?
  3. What does "45" + 5 give, and why? How do you get 50?
  4. In gradeFor, what would go wrong if the mark >= 30 test came before the mark >= 75 test?
  5. SUBJECTS is ["Mathematics", "English", "Biology"]. What is SUBJECTS[1], and what is SUBJECTS.length?
  6. Averages are 91.0, 81.3, 81.3 and 65.7. Using the rule in positionOf, what position does each student get?
  7. Why does the script use textContent rather than innerHTML to put names on the page?
  8. What does event.preventDefault() stop in the submit handler?
  9. A teacher enters marks on the school office computer. Can the head teacher see them on their own phone? Why or why not?
  10. The console shows Cannot read properties of null (reading 'addEventListener'). Name two possible causes.

Answers

  1. Without defer, the script runs as soon as the browser reaches it — before the table and form further down exist — so querySelector would find nothing. defer waits until the whole page has been read.
  2. A const name always refers to the same value; a let name can be given a new one. students uses let because the restore button replaces it with a fresh copy.
  3. "455", because when one side of + is a string, JavaScript joins the two as text. Number("45") + 5 gives 50.
  4. Every mark of 30 or more would stop at that test and get a D, so no one could get an A, B or C.
  5. SUBJECTS[1] is "English", because counting starts at 0. SUBJECTS.length is 3.
  6. 91.0 is 1st. Both students with 81.3 have one student above them, so both are 2nd. 65.7 has three above, so it is 4th.
  7. textContent always shows text as text. innerHTML would treat any HTML or script inside a name as real code, which an attacker could use.
  8. The browser’s normal behaviour for a form: sending it and reloading the page, which would lose what the script is doing.
  9. No. localStorage belongs to one browser on one device. Sharing marks needs a database on a server, which is what Chapter 4 begins.
  10. The id in querySelector does not match an id in the HTML, or the script ran before the page existed because defer is missing.
Chapter 4

Databases with SQL

Store students, subjects and marks, and ask questions of them.

At the end of Chapter 3 the results page could calculate, check and remember — but only in one browser on one computer. A school needs more than that. Every teacher must enter marks into the same place, the head teacher must see them from the office, and nothing must be lost when a laptop is stolen or a browser is cleared. That place is a database. In this chapter you design the database for the results system, build it in PostgreSQL, fill it with the class from Chapter 3, and learn to ask it questions in SQL — the language every database understands.

By the end of this chapter you will be able to:

  • explain what a database does that a spreadsheet or the browser cannot;
  • design tables for students, subjects, exams and marks, and say why they are separate;
  • install PostgreSQL and run SQL in pgAdmin;
  • create tables with rules that stop bad data getting in;
  • add, read, change and delete data;
  • join tables, count, average, group, grade and rank in SQL;
  • save a question as a view that other programs can use;
  • make and restore a backup.

You will keep the files for this chapter in the database folder of your results-system project.

4.1 Why a database

Many schools keep marks in a spreadsheet, and for one teacher that works. Problems start when the work is shared:

ProblemSpreadsheetDatabase
Two teachers enter marks at the same timeTwo copies of the file, and someone’s work is lostBoth write to the same database, safely
A mark of 150 or 7O (with a letter O) is typedAccepted without complaintRefused, because a rule says marks are whole numbers from 0 to 100
The same student appears twiceEasy to do, hard to noticeRefused, because admission numbers must be unique
Questions such as “who is below 45 in two subjects?”Filters and formulas, redone every timeOne query, saved and run again in a second
Thousands of students over many yearsThe file becomes slow and fragileBuilt for millions of rows
Who may see or change whatWhoever has the file has everythingEach user has only the permissions they need

A database is a program that keeps data organised, checks it against your rules, and answers questions about it. The database you will use is PostgreSQL (often just “Postgres”): free, open-source, and trusted by banks, governments and some of the largest websites in the world. ResultsPortal, the results system behind the examples in this book, runs on PostgreSQL.

Where the database lives

A database usually runs on a server, and other programs connect to it:

 Teacher's browser  ──►  Web server program  ──►  PostgreSQL database
  (HTML, CSS, JS)        (checks who you are,       (students, subjects,
                          what you may do)            exams, marks)

The browser never talks to the database directly. If it could, anyone who opened the page could read or delete every mark. A program on the server sits in between and only allows what each person is permitted to do. You will meet that middle program in the coming chapters; in this chapter you talk to the database yourself, as its owner.

4.2 Tables, rows and columns

A database keeps data in tables, which look like the tables you built in Chapter 2:

idadmission_nonamesex
1MSS/2023/001Amina JumaF
2MSS/2023/002Baraka MushiM
3MSS/2023/003Neema KileoF
  • Each column holds one kind of information, and has a name and a type. Every value in the name column is text; every value in id is a whole number.
  • Each row is one thing — here, one student. A row is also called a record.
  • One column, here id, is the primary key: a value that is different for every row, so that each row can be found without any doubt. Two students may both be called Juma Hassan; they will never share an id.

4.3 Designing the results database

The obvious design copies the web page: one big table with a column for each subject.

namesexmathematicsenglishbiology
Amina JumaF788581

It looks simple, but it breaks as soon as the school uses it. When the school adds Kiswahili, the table itself must change. There is nowhere to keep Term 2 except a second copy of every student. And a student’s name is typed again in every table that mentions them, so a spelling correction must be made in many places — and will be missed in one.

The better design gives each kind of thing its own table, and connects them:

 students                 exams               subjects
 ────────────             ─────────           ─────────
 id  ◄──────────┐         id  ◄──────┐        id  ◄──────┐
 admission_no   │         name       │        name       │
 name           │         year       │                   │
 sex            │                    │                   │
                │   marks            │                   │
                │   ─────────        │                   │
                └── student_id       │                   │
                    exam_id ─────────┘                   │
                    subject_id ──────────────────────────┘
                    mark
  • students, subjects and exams each describe one kind of thing, once.
  • marks connects them. Each row says: in this exam, this student got this mark in this subject. It stores ids, not names.
  • A new subject is one new row in subjects. A new exam is one new row in exams. A corrected name is changed in one place, and every result shows the correction.

A column that holds the id of a row in another table is called a foreign key. marks.student_id is a foreign key pointing at students.id. The arrows in the diagram are these connections, called relationships.

4.4 Installing PostgreSQL

  1. Open postgresql.org/download/windows and follow the link to download the installer. Choose the newest version.
  2. Run the installer and accept the defaults. It installs the database server, pgAdmin (a program for working with your databases) and the SQL Shell, also called psql.
  3. When asked for a password for the postgres user, choose a new one and write it down somewhere safe. This user can do anything to every database on your computer. As Chapter 7 explained, never reuse a password from another account.
  4. Keep the port number 5432. At the end, you can untick Stack Builder; you do not need it.

The download is a few hundred megabytes, so use Wi-Fi if you can.

Creating the database

Open pgAdmin. The first time, it asks you to set a master password that protects the passwords it remembers. Then:

  1. In the panel on the left, open Servers, then PostgreSQL, and type the postgres password from the installer.
  2. Right-click Databases and choose Create → Database…. Name it mwangaza and click Save.
  3. Click the new mwangaza database, then choose Tools → Query Tool.

The Query Tool is where you type SQL. To run what you typed, press F5 or click the ▶ button. If the editor contains several statements, pgAdmin runs them all and shows the result of the last one; to run just one, select it with the mouse first and then press F5.

This book shows results the way the SQL Shell prints them. pgAdmin shows the same rows in a grid.

4.5 Creating tables

SQL — usually said “sequel” or “S-Q-L” — is the language for working with databases. A statement is one instruction, and it ends with a semicolon. SQL does not care about capital letters or line breaks: by custom, SQL words are written in capitals and your own names in lowercase.

Create the first table:

CREATE TABLE students (
  id            INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  admission_no  TEXT NOT NULL UNIQUE,
  name          TEXT NOT NULL,
  sex           TEXT NOT NULL CHECK (sex IN ('F', 'M'))
);
CREATE TABLE

Each line inside the brackets is a column: its name, its type, and any rules.

TypeHoldsExample
INTEGERWhole numbers78, 2026
NUMERICExact decimals81.3
TEXTText of any length'Amina Juma'
DATEA date'2026-03-14'
BOOLEANTrue or falsetrue

The rules, called constraints, are what make a database trustworthy:

ConstraintMeans
PRIMARY KEYThis column identifies each row: every value is different, and none is empty
GENERATED ALWAYS AS IDENTITYThe database numbers the rows itself: 1, 2, 3…
NOT NULLA value must be given. NULL means “no value”
UNIQUENo two rows may have the same value
CHECK (…)The value must pass this test
REFERENCES table (column)The value must exist in that other table — a foreign key

In SQL, text values go in single quotes: 'F'. Double quotes mean something else, so do not use them for text.

Now the other three tables:

CREATE TABLE subjects (
  id    INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  TEXT NOT NULL UNIQUE
);

CREATE TABLE exams (
  id    INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  TEXT NOT NULL,
  year  INTEGER NOT NULL,
  UNIQUE (name, year)
);

CREATE TABLE marks (
  exam_id     INTEGER NOT NULL REFERENCES exams (id),
  student_id  INTEGER NOT NULL REFERENCES students (id),
  subject_id  INTEGER NOT NULL REFERENCES subjects (id),
  mark        INTEGER NOT NULL CHECK (mark BETWEEN 0 AND 100),
  PRIMARY KEY (exam_id, student_id, subject_id)
);
CREATE TABLE

CREATE TABLE

CREATE TABLE

Two details deserve a closer look:

  • UNIQUE (name, year) in exams allows a “Term 1 examination” in 2026 and in 2027, but not two in the same year.
  • The primary key of marks is made of three columns together. A student can have many marks, but only one mark for each subject in each exam. Entering the same mark twice is impossible.

In pgAdmin, open mwangaza → Schemas → public → Tables and refresh (right-click, Refresh) to see your four tables.

4.6 Adding data

INSERT adds rows. Name the columns, then give the values in the same order:

INSERT INTO students (admission_no, name, sex) VALUES
  ('MSS/2023/001', 'Amina Juma', 'F'),
  ('MSS/2023/002', 'Baraka Mushi', 'M'),
  ('MSS/2023/003', 'Neema Kileo', 'F'),
  ('MSS/2023/004', 'Juma Hassan', 'M'),
  ('MSS/2023/005', 'Rehema Mollel', 'F');
INSERT 0 5

INSERT 0 5 means five rows were added. (The 0 is left over from old versions of PostgreSQL; ignore it.) We did not give an id: the database chose it. Ask to see the table:

SELECT * FROM students;
 id | admission_no |     name      | sex
----+--------------+---------------+-----
  1 | MSS/2023/001 | Amina Juma    | F
  2 | MSS/2023/002 | Baraka Mushi  | M
  3 | MSS/2023/003 | Neema Kileo   | F
  4 | MSS/2023/004 | Juma Hassan   | M
  5 | MSS/2023/005 | Rehema Mollel | F
(5 rows)

Now the subjects and the exam:

INSERT INTO subjects (name) VALUES
  ('Mathematics'),
  ('English'),
  ('Biology');

INSERT INTO exams (name, year) VALUES
  ('Term 1 examination', 2026);
INSERT 0 3

INSERT 0 1

The subjects are numbered 1 to 3 in the order they were added, and the exam is number 1. The marks refer to everything by these numbers:

-- exam, student, subject, mark
INSERT INTO marks (exam_id, student_id, subject_id, mark) VALUES
  (1, 1, 1, 78), (1, 1, 2, 85), (1, 1, 3, 81),
  (1, 2, 1, 52), (1, 2, 2, 61), (1, 2, 3, 47),
  (1, 3, 1, 91), (1, 3, 2, 88), (1, 3, 3, 94),
  (1, 4, 1, 34), (1, 4, 2, 49), (1, 4, 3, 38),
  (1, 5, 1, 66), (1, 5, 2, 72), (1, 5, 3, 59);
INSERT 0 15

The first row reads: in exam 1, student 1 (Amina Juma) got 78 in subject 1 (Mathematics). These are the same fifteen marks as in Chapters 2 and 3. A line starting with -- is a comment, as // is in JavaScript.

4.7 Asking questions

SELECT asks a question. Its general shape is:

SELECT  which columns
FROM    which table
WHERE   which rows
ORDER BY in what order;

Only SELECT and FROM are required. The girls in the class, in alphabetical order:

SELECT name, admission_no
FROM students
WHERE sex = 'F'
ORDER BY name;
     name      | admission_no
---------------+--------------
 Amina Juma    | MSS/2023/001
 Neema Kileo   | MSS/2023/003
 Rehema Mollel | MSS/2023/005
(3 rows)

* means “every column”, which is handy for a quick look but wasteful in real programs; name the columns you need. In WHERE, a single = compares — unlike JavaScript, SQL has no == or ===. You can combine conditions with AND, OR and NOT, and compare with <, >, <=, >= and <> (not equal).

Every mark below 45, lowest first:

SELECT student_id, subject_id, mark
FROM marks
WHERE mark < 45
ORDER BY mark;
 student_id | subject_id | mark
------------+------------+------
          4 |          1 |   34
          4 |          3 |   38
(2 rows)

The answer is right, but hard to read: who is student 4, and what is subject 3? Section 4.9 fixes that.

Some other useful pieces:

You writeIt means
ORDER BY mark DESCLargest first (descending); ASC, smallest first, is the default
LIMIT 3Only the first three rows of the answer
WHERE mark BETWEEN 45 AND 64From 45 to 64, including both ends
WHERE name LIKE 'J%'Names starting with J; % means “anything”
WHERE sex IN ('F', 'M')Any of the listed values

4.8 Rules that protect the data

The constraints you wrote in section 4.5 are not decoration. Try to break them. A mark of 150:

INSERT INTO marks (exam_id, student_id, subject_id, mark) VALUES (1, 4, 3, 150);
ERROR:  new row for relation "marks" violates check constraint "marks_mark_check"
DETAIL:  Failing row contains (1, 4, 3, 150).

A second Mathematics mark for Amina in the same exam:

INSERT INTO marks (exam_id, student_id, subject_id, mark) VALUES (1, 1, 1, 80);
ERROR:  duplicate key value violates unique constraint "marks_pkey"
DETAIL:  Key (exam_id, student_id, subject_id)=(1, 1, 1) already exists.

A mark for student 9, who does not exist:

INSERT INTO marks (exam_id, student_id, subject_id, mark) VALUES (1, 9, 1, 50);
ERROR:  insert or update on table "marks" violates foreign key constraint "marks_student_id_fkey"
DETAIL:  Key (student_id)=(9) is not present in table "students".

A new student with an admission number that is already taken:

INSERT INTO students (admission_no, name, sex) VALUES ('MSS/2023/001', 'Rose Mwakalinga', 'F');
ERROR:  duplicate key value violates unique constraint "students_admission_no_key"
DETAIL:  Key (admission_no)=(MSS/2023/001) already exists.

Each time, the database refused and said exactly why — and nothing was saved. Read error messages slowly: the first line says what rule was broken, and DETAIL shows the data that broke it.

These rules hold however the data arrives: from pgAdmin, from a web page, from a Python program, or from a mistake in someone’s code years from now. That is why the most important checks belong in the database, even when the web page checks first.

4.9 Joining tables

A join combines rows from several tables, matching them by their keys:

SELECT s.name AS student, sub.name AS subject, m.mark
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
JOIN subjects AS sub ON sub.id = m.subject_id
WHERE m.mark < 45
ORDER BY m.mark;
   student   |   subject   | mark
-------------+-------------+------
 Juma Hassan | Mathematics |   34
 Juma Hassan | Biology     |   38
(2 rows)

Read it from the FROM line:

  • Start with marks, and call it m for short (AS m).
  • For each mark, find the student whose id equals the mark’s student_id, and call that table s.
  • In the same way, find the subject, and call it sub.
  • Now every column of all three tables is available. s.name is the student’s name, sub.name the subject’s name; AS student and AS subject give the result columns clearer headings.

Both students and subjects have a column called name, which is why the short table names are needed: they say which name.

4.10 Counting and averages

Aggregate functions turn many rows into one answer:

SELECT COUNT(*) AS marks,
       MIN(mark) AS lowest,
       MAX(mark) AS highest,
       ROUND(AVG(mark), 1) AS average
FROM marks;
 marks | lowest | highest | average
-------+--------+---------+---------
    15 |     34 |      94 |    66.3
(1 row)

ROUND(…, 1) rounds to one decimal place, like roundTo1 in Chapter 3. The average of all fifteen marks is 66.3 — the class average on your web page.

Groups

GROUP BY makes one answer for each group of rows instead of one for the whole table. The average of each student:

SELECT s.name, ROUND(AVG(m.mark), 1) AS average
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
GROUP BY s.id
ORDER BY average DESC;
     name      | average
---------------+---------
 Neema Kileo   |    91.0
 Amina Juma    |    81.3
 Rehema Mollel |    65.7
 Baraka Mushi  |    53.3
 Juma Hassan   |    40.3
(5 rows)

PostgreSQL knows that one id means one student, so grouping by s.id lets you show s.name too. Compare the averages with Chapter 3’s page: they are the same.

The same idea for subjects:

SELECT sub.name AS subject,
       ROUND(AVG(m.mark), 1) AS average,
       MAX(m.mark) AS highest,
       MIN(m.mark) AS lowest
FROM marks AS m
JOIN subjects AS sub ON sub.id = m.subject_id
GROUP BY sub.id
ORDER BY average DESC;
   subject   | average | highest | lowest
-------------+---------+---------+--------
 English     |    71.0 |      88 |     49
 Mathematics |    64.2 |      91 |     34
 Biology     |    63.8 |      94 |     38
(3 rows)

A head teacher can see at once that Biology has the lowest average this term.

Choosing groups

WHERE chooses rows before they are grouped. To choose groups by their totals or averages, use HAVING, which comes after GROUP BY. Students whose average is below 45:

SELECT s.name, ROUND(AVG(m.mark), 1) AS average
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
GROUP BY s.id
HAVING AVG(m.mark) < 45;
    name     | average
-------------+---------
 Juma Hassan |    40.3
(1 row)

4.11 Grades and positions

CASE makes a decision inside a query, like if and else in JavaScript. It checks each WHEN from the top and uses the first one that is true:

SELECT s.name,
       ROUND(AVG(m.mark), 1) AS average,
       CASE
         WHEN ROUND(AVG(m.mark), 1) >= 75 THEN 'A'
         WHEN ROUND(AVG(m.mark), 1) >= 65 THEN 'B'
         WHEN ROUND(AVG(m.mark), 1) >= 45 THEN 'C'
         WHEN ROUND(AVG(m.mark), 1) >= 30 THEN 'D'
         ELSE 'F'
       END AS grade
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
GROUP BY s.id
ORDER BY s.id;
     name      | average | grade
---------------+---------+-------
 Amina Juma    |    81.3 | A
 Baraka Mushi  |    53.3 | C
 Neema Kileo   |    91.0 | A
 Juma Hassan   |    40.3 | D
 Rehema Mollel |    65.7 | B
(5 rows)

Positions use a window function. RANK() numbers the rows in the order you give it, and rows that tie share a rank, with the next rank skipped — 1, 2, 2, 4, exactly the rule you wrote in positionOf in Chapter 3:

SELECT s.name,
       ROUND(AVG(m.mark), 1) AS average,
       RANK() OVER (ORDER BY ROUND(AVG(m.mark), 1) DESC) AS position
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
GROUP BY s.id
ORDER BY s.id;
     name      | average | position
---------------+---------+----------
 Amina Juma    |    81.3 |        2
 Baraka Mushi  |    53.3 |        4
 Neema Kileo   |    91.0 |        1
 Juma Hassan   |    40.3 |        5
 Rehema Mollel |    65.7 |        3
(5 rows)

The word OVER is what makes it a window function: instead of squashing rows together like AVG, it looks across all of them and gives each row its own answer.

4.12 Functions and views

The grade rule is now written inside a query. Other queries will need it too, and Chapter 3 taught the cure for repeating a rule: put it in a function. SQL has functions as well:

-- The grade for a mark or an average: the same rule as gradeFor in Chapter 3.
CREATE OR REPLACE FUNCTION grade_for(mark NUMERIC) RETURNS TEXT
LANGUAGE SQL IMMUTABLE
RETURN CASE
  WHEN mark >= 75 THEN 'A'
  WHEN mark >= 65 THEN 'B'
  WHEN mark >= 45 THEN 'C'
  WHEN mark >= 30 THEN 'D'
  ELSE 'F'
END;
CREATE FUNCTION
  • grade_for(mark NUMERIC) is the name and the parameter, as in JavaScript.
  • RETURNS TEXT says what kind of value comes back.
  • IMMUTABLE promises that the same mark always gives the same grade, which lets PostgreSQL use the function efficiently.
  • CREATE OR REPLACE means you can run it again after changing the rule.

Test the edges, just as you did in Chapter 3:

SELECT grade_for(75), grade_for(74.9), grade_for(30), grade_for(29);
 grade_for | grade_for | grade_for | grade_for
-----------+-----------+-----------+-----------
 A         | B         | D         | F
(1 row)

Every column of the answer is called grade_for, because no heading was given; AS would name them.

Views

The question “what is each student’s average, grade and position?” will be asked by the web page, by report cards, and by the Python program in Chapter 5. Rather than copying the query into each of them, save it in the database as a view: a named query that you can use exactly like a table.

-- One row for each student in each exam: average, grade and position.
CREATE OR REPLACE VIEW student_results AS
SELECT
  m.exam_id,
  s.id AS student_id,
  s.name,
  s.sex,
  ROUND(AVG(m.mark), 1) AS average,
  grade_for(ROUND(AVG(m.mark), 1)) AS grade,
  RANK() OVER (PARTITION BY m.exam_id ORDER BY ROUND(AVG(m.mark), 1) DESC) AS position
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
GROUP BY m.exam_id, s.id;
CREATE VIEW

Two things are new. The view groups by exam as well as by student, so that when Term 2 is added, each student gets one row per exam. And PARTITION BY m.exam_id ranks each exam separately: positions in Term 2 are not mixed with positions in Term 1.

Now the whole results table is one short question:

SELECT name, average, grade, position
FROM student_results
WHERE exam_id = 1
ORDER BY position;
     name      | average | grade | position
---------------+---------+-------+----------
 Neema Kileo   |    91.0 | A     |        1
 Amina Juma    |    81.3 | A     |        2
 Rehema Mollel |    65.7 | B     |        3
 Baraka Mushi  |    53.3 | C     |        4
 Juma Hassan   |    40.3 | D     |        5
(5 rows)

A view stores the question, not the answers. Change a mark, and the next time anyone reads student_results, the averages, grades and positions are already up to date. Views can be grouped and filtered like any table — how many students got each grade:

SELECT grade, COUNT(*) AS students
FROM student_results
WHERE exam_id = 1
GROUP BY grade
ORDER BY grade;
 grade | students
-------+----------
 A     |        2
 B     |        1
 C     |        1
 D     |        1
(4 rows)

4.13 Changing data safely

UPDATE changes rows that already exist. Juma Hassan’s Biology paper was re-marked, and his mark is 58, not 38:

UPDATE marks
SET mark = 58
WHERE exam_id = 1 AND student_id = 4 AND subject_id = 3;
UPDATE 1

UPDATE 1 means one row was changed. Check the result through the view:

SELECT name, average, grade, position
FROM student_results
WHERE exam_id = 1 AND name = 'Juma Hassan';
    name     | average | grade | position
-------------+---------+-------+----------
 Juma Hassan |    47.0 | C     |        5
(1 row)

His average rose from 40.3 to 47.0 and his grade from D to C — the same change you made on the web page in Chapter 3, now in the database every teacher shares.

DELETE removes rows. It uses WHERE in the same way:

DELETE FROM marks WHERE exam_id = 1 AND student_id = 4 AND subject_id = 3;

Do not run that DELETE yet. Instead, see how to make a change you can still take back.

Transactions

A transaction groups statements so that they all happen, or none of them do. Start one with BEGIN. Until you finish it with COMMIT, you can cancel everything with ROLLBACK:

BEGIN;
DELETE FROM marks;
SELECT COUNT(*) FROM marks;
ROLLBACK;
BEGIN

DELETE 15

 count
-------
     0
(1 row)

ROLLBACK

Inside the transaction every mark was deleted — the count is 0. ROLLBACK cancelled it all. Check:

SELECT COUNT(*) FROM marks;
 count
-------
    15
(1 row)

All fifteen marks are still there. Real systems use transactions whenever several changes belong together. Moving a student from one class to another, for example, may change several tables; if the power fails halfway, the transaction makes sure the database is not left half-changed.

4.14 Backups

Chapter 7 explains the 3-2-1 rule for backups. A database needs it more than anything else on the computer: it is the school’s only record of years of results.

In pgAdmin, right-click the mwangaza database and choose Backup…. Give the file a name with the date, such as mwangaza-2026-03-14.sql, set Format to Plain, and click Backup. A plain backup is an ordinary text file of SQL statements that rebuild the tables and put every row back. You can open it in VS Code and read it.

The same backup from the command line uses pg_dump, which is installed with PostgreSQL:

pg_dump -U postgres -d mwangaza -f mwangaza-2026-03-14.sql

To restore a plain backup, create a new, empty database, open the backup file in its Query Tool, and run it.

A backup also contains personal information about children: names, sexes and marks. Keep backup files as carefully as the database itself — never on a shared computer or in an unprotected email.

4.15 How the web page will reach the database

Your database now holds the class, checks every mark, and answers questions in a moment. The web page from Chapter 3 cannot use it yet, because — as section 4.1 explained — a browser must never connect to a database directly. The missing piece is a program on a server that:

  1. receives a request from the page, such as “save mark 58 for student 4 in Biology”;
  2. checks who is asking and whether they are allowed to do it;
  3. runs the SQL, with the values passed separately from the SQL text;
  4. sends the answer back to the page.

Step 3 matters for security. If a program builds SQL by gluing typed text into it, someone can type SQL instead of a mark, and the database will run it. This attack is called SQL injection, and it is one of the most common ways websites are broken into. Every database library has a safe way to pass values separately; Chapter 5 uses it from the first line of Python.

4.16 The complete files

Save these three files in your database folder. Together they rebuild the database from nothing: open each in the Query Tool and run them in this order.

database/schema.sql

-- Mwangaza Secondary School - results database
-- Run on a practice database. The first line deletes these tables, so the
-- file can be run again to start over. Never run it on real school data.

DROP TABLE IF EXISTS marks, exams, subjects, students CASCADE;

CREATE TABLE students (
  id            INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  admission_no  TEXT NOT NULL UNIQUE,
  name          TEXT NOT NULL,
  sex           TEXT NOT NULL CHECK (sex IN ('F', 'M'))
);

CREATE TABLE subjects (
  id    INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  TEXT NOT NULL UNIQUE
);

CREATE TABLE exams (
  id    INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  TEXT NOT NULL,
  year  INTEGER NOT NULL,
  UNIQUE (name, year)
);

CREATE TABLE marks (
  exam_id     INTEGER NOT NULL REFERENCES exams (id),
  student_id  INTEGER NOT NULL REFERENCES students (id),
  subject_id  INTEGER NOT NULL REFERENCES subjects (id),
  mark        INTEGER NOT NULL CHECK (mark BETWEEN 0 AND 100),
  PRIMARY KEY (exam_id, student_id, subject_id)
);

DROP TABLE deletes tables and everything in them; CASCADE also deletes anything built on them, such as the view. That is what makes the file safe to run again on a practice database — and dangerous anywhere else.

database/data.sql

-- Mwangaza Secondary School - Form 4, Term 1 examination, 2026
-- Run after schema.sql. The database numbers rows in the order they are
-- added, so the students are 1 to 5, the subjects 1 to 3 and the exam 1.

INSERT INTO students (admission_no, name, sex) VALUES
  ('MSS/2023/001', 'Amina Juma', 'F'),
  ('MSS/2023/002', 'Baraka Mushi', 'M'),
  ('MSS/2023/003', 'Neema Kileo', 'F'),
  ('MSS/2023/004', 'Juma Hassan', 'M'),
  ('MSS/2023/005', 'Rehema Mollel', 'F');

INSERT INTO subjects (name) VALUES
  ('Mathematics'),
  ('English'),
  ('Biology');

INSERT INTO exams (name, year) VALUES
  ('Term 1 examination', 2026);

-- exam, student, subject, mark
INSERT INTO marks (exam_id, student_id, subject_id, mark) VALUES
  (1, 1, 1, 78), (1, 1, 2, 85), (1, 1, 3, 81),
  (1, 2, 1, 52), (1, 2, 2, 61), (1, 2, 3, 47),
  (1, 3, 1, 91), (1, 3, 2, 88), (1, 3, 3, 94),
  (1, 4, 1, 34), (1, 4, 2, 49), (1, 4, 3, 38),
  (1, 5, 1, 66), (1, 5, 2, 72), (1, 5, 3, 59);

database/results.sql

-- Mwangaza Secondary School - grades, and the results of every exam
-- Run after schema.sql. Chapter 5 reads student_results from Python.

-- The grade for a mark or an average: the same rule as gradeFor in Chapter 3.
CREATE OR REPLACE FUNCTION grade_for(mark NUMERIC) RETURNS TEXT
LANGUAGE SQL IMMUTABLE
RETURN CASE
  WHEN mark >= 75 THEN 'A'
  WHEN mark >= 65 THEN 'B'
  WHEN mark >= 45 THEN 'C'
  WHEN mark >= 30 THEN 'D'
  ELSE 'F'
END;

-- One row for each student in each exam: average, grade and position.
CREATE OR REPLACE VIEW student_results AS
SELECT
  m.exam_id,
  s.id AS student_id,
  s.name,
  s.sex,
  ROUND(AVG(m.mark), 1) AS average,
  grade_for(ROUND(AVG(m.mark), 1)) AS grade,
  RANK() OVER (PARTITION BY m.exam_id ORDER BY ROUND(AVG(m.mark), 1) DESC) AS position
FROM marks AS m
JOIN students AS s ON s.id = m.student_id
GROUP BY m.exam_id, s.id;

After running all three, this query gives the same table as the web page in Chapter 3:

SELECT name, average, grade, position
FROM student_results
WHERE exam_id = 1
ORDER BY position;
     name      | average | grade | position
---------------+---------+-------+----------
 Neema Kileo   |    91.0 | A     |        1
 Amina Juma    |    81.3 | A     |        2
 Rehema Mollel |    65.7 | B     |        3
 Baraka Mushi  |    53.3 | C     |        4
 Juma Hassan   |    40.3 | D     |        5
(5 rows)

Try it yourself

  1. Add Kiswahili as a subject, and give every student a Kiswahili mark in exam 1. Which statements did you need — and did you have to change any table?
  2. Add a “Term 2 examination” for 2026 with marks for every student, and show each student’s Term 1 and Term 2 averages. Check that positions in student_results are worked out separately for each exam.
  3. Write a query that lists every student with at least one mark below 30, with the subject. (Hint: a join and a WHERE.)
  4. Count the students of each sex who got each grade in exam 1 — a table head teachers are often asked for.
  5. Make a backup, create a new database called mwangaza_restore, restore the backup into it, and check that student_results gives the same answers.

Key terms

EnglishKiswahiliMeaning
DatabaseHifadhidataAn organised store of data that checks it and answers questions about it
TableJedwaliData about one kind of thing, in rows and columns
Row (record)RekodiOne item in a table, such as one student
ColumnSafu wimaOne kind of information in a table, such as name
Primary keyUfunguo mkuuA column whose value is different for every row
Foreign keyUfunguo wa njeA column that holds the key of a row in another table
RelationshipUhusianoA connection between two tables through keys
QueryHojaA question asked of the database in SQL
ConstraintKizuiziA rule the data must obey, such as CHECK
ViewMwonekanoA saved query that can be used like a table
TransactionMuamalaSeveral changes that all happen, or none do
BackupNakala rudufuA copy of the database kept somewhere safe

Check your understanding

  1. Give two problems with keeping a school’s marks in one shared spreadsheet that a database solves.
  2. Why does the marks table store student_id instead of the student’s name?
  3. What stops a teacher entering two Mathematics marks for Amina in the same exam?
  4. What is the difference between WHERE and HAVING?
  5. Write a query that lists the names of the boys, in alphabetical order.
  6. Averages are 91.0, 81.3, 81.3 and 65.7. What positions does RANK() give them?
  7. What is the difference between a table and a view? Why does a view never show an out-of-date average?
  8. You meant to run DELETE FROM marks WHERE student_id = 4; but forgot the WHERE. What happens, and what habit would have protected you?
  9. Why must a web page never connect directly to the database?
  10. What is SQL injection, and how is it prevented?

Answers

  1. For example: two teachers editing copies of the file lose each other’s work; impossible values such as 150 are accepted; the same student can be entered twice. A database stores one shared copy, refuses data that breaks its rules, and answers questions quickly.
  2. A name stored in one place can be corrected in one place, and two students with the same name are never confused. The id connects each mark to exactly one student.
  3. The primary key of marks is (exam_id, student_id, subject_id), so there can be only one row for each student, subject and exam.
  4. WHERE chooses rows before they are grouped. HAVING chooses groups after GROUP BY, using their totals or averages.
  5. SELECT name FROM students WHERE sex = 'M' ORDER BY name;
  6. 1, 2, 2 and 4. The two students with 81.3 share 2nd place, and the next position, 3rd, is skipped.
  7. A table stores data. A view stores a query, which runs again each time the view is read, so it always uses the current marks.
  8. Every mark in the table is deleted. Starting with BEGIN; — and checking with a SELECT before COMMIT; — would have let you ROLLBACK;. So would running a SELECT with the same WHERE first.
  9. Anyone who opened the page could then read, change or delete everything. A server program in between checks who is asking and allows only what they are permitted to do.
  10. An attack where someone types SQL into a form, and a program that glues typed text into its SQL runs it. It is prevented by passing values separately from the SQL text, as every database library allows.
Chapter 5

Python

Import a class list, and calculate grades and divisions automatically.

The web page from Chapter 3 works out averages for one class as a teacher types. A school needs more: at the end of every term, hundreds of marks arrive in spreadsheets, and someone must grade every subject, find each student’s best seven, work out divisions, and produce lists for the head teacher and the noticeboard. Done by hand, that is days of work and a mistake on every page. In this chapter you write a Python program that does it in a second, checks the file for typing mistakes first, and saves everything into the database you built in Chapter 4.

By the end of this chapter you will be able to:

  • install Python and run programs from a terminal;
  • use variables, lists, dictionaries, loops and functions in Python;
  • explain how divisions are worked out from the best seven subjects;
  • read a spreadsheet saved as CSV, and write one;
  • catch bad data with exceptions and clear error messages;
  • test your own functions automatically;
  • connect to PostgreSQL from Python safely, with no password in the code.

The files for this chapter go in the python folder of your results-system project.

5.1 Why Python

You already know one programming language, JavaScript, which runs in the browser. Python runs on the computer itself, where it can open files, read spreadsheets, talk to databases and run on a schedule with nobody watching. It is famous for being easy to read — many people say it looks like instructions written in English — and it is used everywhere from school offices to banks, scientific research and artificial intelligence.

Most of what you learned in Chapter 3 carries over. Here is the same grade rule in both languages:

JavaScript (Chapter 3)Python (this chapter)
function gradeFor(mark) {def grade_for(mark):
if (mark >= 75) {if mark >= 75:
} else if (mark >= 65) {elif mark >= 65:
return "A";return "A"
const marks = [78, 85];marks = [78, 85]
marks.lengthlen(marks)
// a comment# a comment

Two differences matter more than the rest:

  • Python uses indentation instead of curly braces. The lines inside an if or a function are the lines indented under it. The indentation is not decoration: get it wrong and the program means something else, or will not run.
  • Python names use underscores: grade_for, class_list, best_subjects. This style is called snake case.

5.2 Installing Python

  1. Open python.org/downloads and download the newest Python 3 for Windows.
  2. Run the installer. On the first screen, tick “Add python.exe to PATH” before you click Install Now. Without it, typing python in a terminal will not find Python.
  3. In VS Code, open the Extensions panel (Ctrl + Shift + X), search for Python, and install the one published by Microsoft. It colours your code and points out mistakes as you type.

The terminal

Python programs are usually run from a terminal: a window where you type commands instead of clicking. VS Code has one built in. Open your results-system folder, then press Ctrl + ` (the backtick key, left of 1). Check that Python is installed:

python --version
Python 3.14.7

Your version number may be newer. If you see “Python was not found”, run the installer again and tick the PATH box.

A few terminal commands you will use all the time:

CommandWhat it does
cd pythonMove into the python folder (change directory)
cd ..Move up one folder
dirList the files in this folder (ls on Linux and macOS)
python report.pyRun the program in report.py
↑Bring back the last command, to run it again

Your first program

Create hello.py in the python folder:

name = "Mwl. Grace Mushi"
students = 10
print(f"Habari, {name}! You have {students} students in Form 4.")

Move into the folder and run it:

cd python
python hello.py
Habari, Mwl. Grace Mushi! You have 10 students in Form 4.

The f before the quotes makes an f-string: everything inside { } is replaced by its value. It is the same idea as JavaScript’s template strings in Chapter 3.

5.3 Values, lists and dictionaries

Python has the same basic values as JavaScript, with slightly different names:

TypeExamplesNotes
int78, -5Whole numbers
float81.3Numbers with a decimal point
str"Amina Juma"Text, in double or single quotes
boolTrue, FalseCapital letters in Python
NoneNone“No value” — like null

A list holds values in order, counted from zero:

marks = [82, 76, 79, 88, 85]
marks[0]       # 82
len(marks)     # 5
sum(marks)     # 410
sorted(marks)  # [76, 79, 82, 85, 88]

A dictionary holds values under names, called keys — like a JavaScript object:

student = {"name": "Amina Juma", "sex": "F", "marks": {"Biology": 81, "English": 85}}
student["name"]               # "Amina Juma"
student["marks"]["Biology"]   # 81

Loops

for takes each item in turn, exactly like for...of in JavaScript:

for subject, mark in student["marks"].items():
    print(subject, mark)

.items() gives each key with its value, so each time round the loop subject is a subject’s name and mark is its mark.

Python also has a compact way to build a new list from an old one, called a list comprehension:

grades = [grade_for(mark) for mark in marks]

Read it as: “a list of grade_for(mark), for each mark in marks”. It does in one line what a loop and an empty list would do in four.

5.4 Grades and divisions

Create grades.py. The grade rule is the one from Chapters 3 and 4:

def grade_for(mark):
    """The letter grade for a mark from 0 to 100."""
    if mark >= 75:
        return "A"
    elif mark >= 65:
        return "B"
    elif mark >= 45:
        return "C"
    elif mark >= 30:
        return "D"
    else:
        return "F"

def starts a function. The text in triple quotes just under it is a docstring: a description of what the function does, which editors show when you use the function.

How divisions work

In Form 4, a student’s overall result is a division. Each grade is worth points — the fewer the better:

GradeABCDF
Points12345

Only the student’s best seven subjects count. Their points are added up, and the total decides the division:

Total pointsDivision
7 to 17I
18 to 21II
22 to 25III
26 to 33IV
34 to 350

Seven A grades give 7 points, the best possible result. Seven F grades give 35, the worst. A student who sat fewer than seven subjects has no division yet; we mark that INC, for incomplete.

In Python:

GRADE_POINTS = {"A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
BEST_SUBJECTS = 7


def division_for(grades):
    """The points of the best seven grades, and the division they earn.

    A student with fewer than seven grades has no division yet: (None, "INC").
    """
    if len(grades) < BEST_SUBJECTS:
        return None, "INC"
    best = sorted(GRADE_POINTS[grade] for grade in grades)[:BEST_SUBJECTS]
    points = sum(best)
    if points <= 17:
        division = "I"
    elif points <= 21:
        division = "II"
    elif points <= 25:
        division = "III"
    elif points <= 33:
        division = "IV"
    else:
        division = "0"
    return points, division

The key line is the one that finds the best seven:

  • GRADE_POINTS[grade] for grade in grades turns each grade into its points;
  • sorted(...) puts them in order, smallest — best — first;
  • [:BEST_SUBJECTS] is a slice: the first seven items.

A function can give back two values at once, separated by a comma. The caller unpacks them the same way: points, division = division_for(grades).

5.5 Reading a spreadsheet

Schools keep marks in Excel or Google Sheets. Python can read those files directly with extra libraries, but the simplest and most reliable route is CSV — “comma-separated values” — which every spreadsheet program can save. In Excel, choose File → Save As, and pick CSV UTF-8 (Comma delimited). In Google Sheets, choose File → Download → Comma-separated values.

A CSV file is plain text: one line for each row, with commas between the cells. Here is the class list for this chapter, class_list.csv, as it looks in a text editor:

admission_no,name,sex,Civics,History,Geography,Kiswahili,English,Biology,Mathematics,Chemistry,Physics
MSS/2023/001,Amina Juma,F,82,76,79,88,85,81,78,74,70
MSS/2023/002,Baraka Mushi,M,58,49,61,66,61,47,52,44,40
MSS/2023/003,Neema Kileo,F,90,85,88,92,88,94,91,86,89
MSS/2023/004,Juma Hassan,M,38,41,35,52,49,38,34,29,31
MSS/2023/005,Rehema Mollel,F,68,70,63,75,72,59,66,58,61
MSS/2023/006,Daudi Mrisho,M,25,31,28,40,33,22,18,15,20
MSS/2023/007,Zawadi Ngowi,F,20,18,25,28,22,19,12,10,15
MSS/2023/008,Elia Kimaro,M,55,62,,58,64,51,45,48,50
MSS/2023/009,Faraja Lyimo,M,50,46,35,55,48,41,38,30,44
MSS/2023/010,Halima Said,F,70,,65,80,,72,60,,

The first line holds the column headings. An empty cell — two commas in a row, as in Elia Kimaro’s Geography — means the student did not sit that paper. Halima Said joined the school mid-term and sat only five subjects.

Python’s built-in csv module reads it. csv.DictReader turns each row into a dictionary, using the headings as keys:

import csv

with open("class_list.csv", newline="", encoding="utf-8-sig") as file:
    for row in csv.DictReader(file):
        print(row["name"], row["Biology"])
  • import csv loads the module. Python comes with hundreds of modules like it, called the standard library.
  • with open(...) as file: opens the file and closes it again when the indented block ends, even if something goes wrong.
  • encoding="utf-8-sig" reads the text correctly whichever way Excel saved it. Excel’s “CSV UTF-8” adds an invisible marker at the start of the file; utf-8-sig removes it, so the first heading is not read as admission_no.

Every value read from a CSV file is a string, even the marks: row["Biology"] is "81", not 81. Turning it into a number is the next job — and the place where typing mistakes are caught.

5.6 Catching bad data

Real class lists contain mistakes: 7O typed with a letter O, 147 with an extra digit, a mark in the wrong column. A program that quietly accepts them produces wrong results that look right. A good program refuses, and says exactly where the problem is.

def parse_mark(text, where):
    """A mark from the file: a whole number from 0 to 100, or None if blank."""
    text = text.strip()
    if text == "":
        return None
    try:
        mark = int(text)
    except ValueError:
        raise ValueError(f"{where}: '{text}' is not a whole number")
    if mark < 0 or mark > 100:
        raise ValueError(f"{where}: {mark} is not between 0 and 100")
    return mark
  • int(text) turns "81" into 81. If the text is not a whole number, it raises an exception called ValueError.
  • try: and except ValueError: catch that exception. Instead of Python’s own message, we raise a new one that says where the problem is, such as line 5, Civics.
  • raise stops the function and sends the error to whoever called it. Something further up must catch it, or the program stops.

The function that reads the whole file calls parse_mark for every cell, and counts lines so the message can name the right one:

def read_class(path):
    """The subjects, and one dictionary for each student in the file."""
    with open(path, newline="", encoding="utf-8-sig") as file:
        reader = csv.DictReader(file)
        subjects = [column for column in reader.fieldnames if column not in INFO_COLUMNS]
        students = []
        for line, row in enumerate(reader, start=2):
            marks = {}
            for subject in subjects:
                mark = parse_mark(row[subject], f"line {line}, {subject}")
                if mark is not None:
                    marks[subject] = mark
            students.append({
                "admission_no": row["admission_no"],
                "name": row["name"],
                "sex": row["sex"],
                "marks": marks,
            })
    return subjects, students

enumerate counts as it loops. It starts at 2 because line 1 of the file is the headings, so the first student is on line 2 — the same number a teacher sees in Excel’s row numbers. The subjects are every column except admission_no, name and sex, so a school with eleven subjects, or different ones, needs no change to the code.

Here is what the finished program says when Juma Hassan’s Civics mark was typed as 3B instead of 38:

Could not read the class list: line 5, Civics: '3B' is not a whole number

and when a Biology mark was typed as 147:

Could not read the class list: line 3, Biology: 147 is not between 0 and 100

The teacher can go straight to the right cell and fix it. Nothing is calculated from a file with a mistake in it.

5.7 The report

Now put it together. For each student, results_for grades every mark, works out the division, and adds the average:

def results_for(student):
    """The student, with their average, points and division added."""
    marks = student["marks"]
    grades = [grade_for(mark) for mark in marks.values()]
    points, division = division_for(grades)
    average = round(sum(marks.values()) / len(marks), 1) if marks else 0.0
    return {**student, "average": average, "points": points, "division": division}

{**student, ...} makes a new dictionary containing everything in student, plus the new keys. The original is left as it was.

To list the best results first, Python’s sorted takes a key: a function that says what to sort by.

def sort_key(result):
    """Best first: fewest points, then highest average. Incomplete results last."""
    if result["points"] is None:
        return (1, 0, -result["average"])
    return (0, result["points"], -result["average"])

Python compares these tuples — values in round brackets — item by item. Complete results (0) come before incomplete ones (1); then fewer points come first; then a minus sign turns the highest average into the smallest number, so it comes first.

main reads the file, prints the table and a summary, and saves the results. The {name:<15} and {average:>8.1f} inside the f-strings set the width of each column: < lines text up on the left, > lines numbers up on the right, and .1f shows one decimal place. The complete report.py is in section 5.11. Run it:

python report.py
10 students, 9 subjects

No. Name           Sex  Average  Points  Division
1   Neema Kileo    F       89.2       7  I
2   Amina Juma     F       79.2       7  I
3   Rehema Mollel  F       65.8      15  I
4   Baraka Mushi   M       53.1      20  II
5   Elia Kimaro    M       54.1      21  II
6   Faraja Lyimo   M       43.0      24  III
7   Juma Hassan    M       38.6      26  IV
8   Daudi Mrisho   M       25.8      32  IV
9   Zawadi Ngowi   F       18.8      35  0
10  Halima Said    F       69.4       -  INC

Divisions   I: 3   II: 2   III: 1   IV: 2   0: 1   INC: 1
Saved results.csv

Look at Elia Kimaro: his average is higher than Baraka Mushi’s, but he is placed below him. Elia missed Geography, so all seven of his counted subjects are C grades — 21 points — while Baraka’s best seven include a B, for 20 points. Divisions reward the best seven subjects, not the average. Halima Said has a good average, but with five subjects she has no division yet.

The program also wrote results.csv, which opens in Excel:

admission_no,name,sex,subjects,average,points,division
MSS/2023/003,Neema Kileo,F,9,89.2,7,I
MSS/2023/001,Amina Juma,F,9,79.2,7,I
MSS/2023/005,Rehema Mollel,F,9,65.8,15,I
MSS/2023/002,Baraka Mushi,M,9,53.1,20,II
MSS/2023/008,Elia Kimaro,M,8,54.1,21,II
MSS/2023/009,Faraja Lyimo,M,9,43.0,24,III
MSS/2023/004,Juma Hassan,M,9,38.6,26,IV
MSS/2023/006,Daudi Mrisho,M,9,25.8,32,IV
MSS/2023/007,Zawadi Ngowi,F,9,18.8,35,0
MSS/2023/010,Halima Said,F,5,69.4,,INC

Modules and __main__

report.py starts with from grades import division_for, grade_for. Any Python file is a module that other files can import, which is how grades.py is shared by the report, the tests and the database program. The last two lines of report.py are:

if __name__ == "__main__":
    main()

When you run python report.py, Python sets __name__ to "__main__", so main() runs. When another file imports report, __name__ is "report" instead, and nothing runs by itself — the other file can use read_class without printing a report.

5.8 Testing your functions

How do you know division_for is right — not just for these ten students, but at every boundary? Write code that checks it. Python’s built-in unittest module runs checks for you. Create test_grades.py:

"""Checks for grades.py. Run with: python -m unittest"""
import unittest

from grades import division_for, grade_for


class GradeTests(unittest.TestCase):
    def test_grade_boundaries(self):
        self.assertEqual(grade_for(75), "A")
        self.assertEqual(grade_for(74), "B")
        self.assertEqual(grade_for(65), "B")
        self.assertEqual(grade_for(45), "C")
        self.assertEqual(grade_for(44), "D")
        self.assertEqual(grade_for(30), "D")
        self.assertEqual(grade_for(29), "F")


class DivisionTests(unittest.TestCase):
    def test_only_the_best_seven_count(self):
        self.assertEqual(division_for(["A"] * 7 + ["F", "F"]), (7, "I"))

    def test_division_boundaries(self):
        self.assertEqual(division_for(["A"] + ["B"] * 2 + ["C"] * 4), (17, "I"))
        self.assertEqual(division_for(["B"] * 3 + ["C"] * 4), (18, "II"))
        self.assertEqual(division_for(["D"] * 2 + ["F"] * 5), (33, "IV"))
        self.assertEqual(division_for(["D"] + ["F"] * 6), (34, "0"))

    def test_fewer_than_seven_subjects(self):
        self.assertEqual(division_for(["A"] * 6), (None, "INC"))


if __name__ == "__main__":
    unittest.main()

Each method whose name starts with test_ is one test, and each assertEqual checks that two things are equal. ["A"] * 7 is a list of seven "A"s. Run every test in the folder:

python -m unittest
....
----------------------------------------------------------------------
Ran 4 tests in 0.003s

OK

Each dot is a test that passed. If one fails, you see an F and a message showing what was expected and what came back. Run the tests every time you change grades.py: if someone “fixes” the boundary between Division I and II next year, the tests say at once whether they broke anything.

5.9 Saving into the database

The report is useful, but the results still live in files on one computer. The database from Chapter 4 is where they belong. Python talks to PostgreSQL through a library called psycopg. Install it from the terminal:

python -m pip install "psycopg[binary]"

pip is Python’s tool for installing libraries from the Python Package Index. The [binary] part installs a ready-made version that needs nothing else on Windows.

Keep the password out of the code

The program needs the database password, and it must never be written in the code. Code is copied, shared, emailed and — in Chapter 8 — published on GitHub, and a password written in it goes everywhere the code goes. Instead, the password is given to the program through an environment variable: a setting that belongs to the terminal window, not to the file. psycopg reads PGUSER and PGPASSWORD by itself.

In VS Code’s terminal on Windows, which uses PowerShell, type these two lines, using your own password from Chapter 4:

$env:PGUSER = "postgres"
$env:PGPASSWORD = "your-password-here"

They last until you close that terminal window, and they are never saved in your project. On Linux or macOS, the same is written export PGUSER=postgres.

Values go separately from the SQL

Chapter 4 warned about SQL injection. In Python it looks like this. Never do this:

# WRONG: the name becomes part of the SQL itself
cur.execute(f"INSERT INTO students (name) VALUES ('{name}')")

If a “name” in the file contains a quote and some SQL, the database runs it. The safe way is to write %s where each value goes, and pass the values separately:

# RIGHT: psycopg sends the values separately from the SQL
cur.execute("INSERT INTO students (name) VALUES (%s)", (name,))

The values travel to the database apart from the SQL text, so nothing in them can ever be run as SQL — whatever it contains. The comma in (name,) matters: it makes a tuple with one item.

The program

save_to_db.py saves the class list as a new exam, the “Mock examination”. For each student, subject and mark it uses INSERT ... ON CONFLICT ... DO UPDATE, which adds the row if it is new and updates it if it already exists. That makes the program safe to run twice: the second run corrects marks rather than adding them again. Its main part is:

    with psycopg.connect(host="localhost", dbname="mwangaza") as conn:
        exam_id = save(conn, subjects, students)
        top = conn.execute(
            """SELECT position, name, average, grade FROM student_results
               WHERE exam_id = %s ORDER BY position LIMIT 3""",
            (exam_id,),
        ).fetchall()

The with block is also a transaction, as in Chapter 4: if anything fails before the end — a mark that breaks a rule, a lost connection — nothing at all is saved. Only when the block finishes does psycopg commit every change at once. Run it:

python save_to_db.py
Saved 10 students and 9 subjects for the Mock examination, 2026.
Top three:
  1. Neema Kileo, average 89.2, grade A
  2. Amina Juma, average 79.2, grade A
  3. Halima Said, average 69.4, grade B

The marks are now in the shared database. Every teacher’s screen, every report card and the web page can read them from student_results — the view you wrote in Chapter 4, which ranked this new exam separately from Term 1 because of its PARTITION BY exam_id.

Notice the third place. The view ranks by average and knows nothing about divisions, so Halima Said — five subjects, no division — comes third. The Python report placed her last. Neither is wrong; they answer different questions. A real system must decide which one it shows to parents, and say so on the page.

5.10 When something goes wrong

MessageWhat it means
IndentationError: expected an indented block after 'if' statement on line 2The line after an if, for or def is not indented, or spaces and tabs are mixed. VS Code’s status bar shows which you are using
NameError: name 'grade_for' is not definedA name is misspelt, or the import is missing
ModuleNotFoundError: No module named 'psycopg'The library is not installed: run the pip install command in section 5.9
FileNotFoundError: [Errno 2] No such file or directory: 'class_list.csv'The terminal is in a different folder from the file. Use cd to move into the python folder
TypeError: can only concatenate str (not "int") to strA string and a number were joined with +. Turn the number into text with str(), or use an f-string
KeyError: 'Biology'A dictionary has no such key — check the spelling and capitals of the CSV heading
connection failed: ... password authentication failedPGPASSWORD is not set in this terminal window, or is wrong

Python’s own error messages end with the most useful line: read from the bottom up. The lines above it, called the traceback, show which file and line the problem was found on.

5.11 The complete files

The python folder at the end of this chapter holds five files: the class list, the grade rules, the report, the tests and the database program. report.py, test_grades.py and save_to_db.py all import from grades.py or report.py, so keep them together in one folder.

python/class_list.csv

admission_no,name,sex,Civics,History,Geography,Kiswahili,English,Biology,Mathematics,Chemistry,Physics
MSS/2023/001,Amina Juma,F,82,76,79,88,85,81,78,74,70
MSS/2023/002,Baraka Mushi,M,58,49,61,66,61,47,52,44,40
MSS/2023/003,Neema Kileo,F,90,85,88,92,88,94,91,86,89
MSS/2023/004,Juma Hassan,M,38,41,35,52,49,38,34,29,31
MSS/2023/005,Rehema Mollel,F,68,70,63,75,72,59,66,58,61
MSS/2023/006,Daudi Mrisho,M,25,31,28,40,33,22,18,15,20
MSS/2023/007,Zawadi Ngowi,F,20,18,25,28,22,19,12,10,15
MSS/2023/008,Elia Kimaro,M,55,62,,58,64,51,45,48,50
MSS/2023/009,Faraja Lyimo,M,50,46,35,55,48,41,38,30,44
MSS/2023/010,Halima Said,F,70,,65,80,,72,60,,

python/grades.py

"""Grades, points and divisions for Form 4 results."""

GRADE_POINTS = {"A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
BEST_SUBJECTS = 7


def grade_for(mark):
    """The letter grade for a mark from 0 to 100."""
    if mark >= 75:
        return "A"
    elif mark >= 65:
        return "B"
    elif mark >= 45:
        return "C"
    elif mark >= 30:
        return "D"
    else:
        return "F"


def division_for(grades):
    """The points of the best seven grades, and the division they earn.

    A student with fewer than seven grades has no division yet: (None, "INC").
    """
    if len(grades) < BEST_SUBJECTS:
        return None, "INC"
    best = sorted(GRADE_POINTS[grade] for grade in grades)[:BEST_SUBJECTS]
    points = sum(best)
    if points <= 17:
        division = "I"
    elif points <= 21:
        division = "II"
    elif points <= 25:
        division = "III"
    elif points <= 33:
        division = "IV"
    else:
        division = "0"
    return points, division

python/report.py

"""Read a class list, work out every student's results, and save them."""
import csv
import sys

from grades import division_for, grade_for

INFO_COLUMNS = ["admission_no", "name", "sex"]


def parse_mark(text, where):
    """A mark from the file: a whole number from 0 to 100, or None if blank."""
    text = text.strip()
    if text == "":
        return None
    try:
        mark = int(text)
    except ValueError:
        raise ValueError(f"{where}: '{text}' is not a whole number")
    if mark < 0 or mark > 100:
        raise ValueError(f"{where}: {mark} is not between 0 and 100")
    return mark


def read_class(path):
    """The subjects, and one dictionary for each student in the file."""
    with open(path, newline="", encoding="utf-8-sig") as file:
        reader = csv.DictReader(file)
        subjects = [column for column in reader.fieldnames if column not in INFO_COLUMNS]
        students = []
        for line, row in enumerate(reader, start=2):
            marks = {}
            for subject in subjects:
                mark = parse_mark(row[subject], f"line {line}, {subject}")
                if mark is not None:
                    marks[subject] = mark
            students.append({
                "admission_no": row["admission_no"],
                "name": row["name"],
                "sex": row["sex"],
                "marks": marks,
            })
    return subjects, students


def results_for(student):
    """The student, with their average, points and division added."""
    marks = student["marks"]
    grades = [grade_for(mark) for mark in marks.values()]
    points, division = division_for(grades)
    average = round(sum(marks.values()) / len(marks), 1) if marks else 0.0
    return {**student, "average": average, "points": points, "division": division}


def sort_key(result):
    """Best first: fewest points, then highest average. Incomplete results last."""
    if result["points"] is None:
        return (1, 0, -result["average"])
    return (0, result["points"], -result["average"])


def save_results(path, results):
    with open(path, "w", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        writer.writerow(["admission_no", "name", "sex", "subjects", "average", "points", "division"])
        for r in results:
            writer.writerow([r["admission_no"], r["name"], r["sex"], len(r["marks"]),
                             r["average"], r["points"] or "", r["division"]])


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "class_list.csv"
    try:
        subjects, students = read_class(path)
    except (OSError, ValueError) as error:
        print(f"Could not read the class list: {error}")
        sys.exit(1)

    results = sorted((results_for(s) for s in students), key=sort_key)

    print(f"{len(results)} students, {len(subjects)} subjects\n")
    print(f"{'No.':<4}{'Name':<15}{'Sex':<4}{'Average':>8}{'Points':>8}  Division")
    for number, r in enumerate(results, start=1):
        points = "-" if r["points"] is None else r["points"]
        print(f"{number:<4}{r['name']:<15}{r['sex']:<4}{r['average']:>8.1f}{points:>8}  {r['division']}")

    counts = {}
    for r in results:
        counts[r["division"]] = counts.get(r["division"], 0) + 1
    summary = "   ".join(f"{d}: {counts.get(d, 0)}" for d in ["I", "II", "III", "IV", "0", "INC"])
    print(f"\nDivisions   {summary}")

    save_results("results.csv", results)
    print("Saved results.csv")


if __name__ == "__main__":
    main()

sys.argv is the list of words typed after python on the command line. With python report.py form3.csv, sys.argv[1] is "form3.csv", so the same program reads any class list; with no name, it reads class_list.csv. sys.exit(1) stops the program and tells the terminal it failed.

python/test_grades.py

"""Checks for grades.py. Run with: python -m unittest"""
import unittest

from grades import division_for, grade_for


class GradeTests(unittest.TestCase):
    def test_grade_boundaries(self):
        self.assertEqual(grade_for(75), "A")
        self.assertEqual(grade_for(74), "B")
        self.assertEqual(grade_for(65), "B")
        self.assertEqual(grade_for(45), "C")
        self.assertEqual(grade_for(44), "D")
        self.assertEqual(grade_for(30), "D")
        self.assertEqual(grade_for(29), "F")


class DivisionTests(unittest.TestCase):
    def test_only_the_best_seven_count(self):
        self.assertEqual(division_for(["A"] * 7 + ["F", "F"]), (7, "I"))

    def test_division_boundaries(self):
        self.assertEqual(division_for(["A"] + ["B"] * 2 + ["C"] * 4), (17, "I"))
        self.assertEqual(division_for(["B"] * 3 + ["C"] * 4), (18, "II"))
        self.assertEqual(division_for(["D"] * 2 + ["F"] * 5), (33, "IV"))
        self.assertEqual(division_for(["D"] + ["F"] * 6), (34, "0"))

    def test_fewer_than_seven_subjects(self):
        self.assertEqual(division_for(["A"] * 6), (None, "INC"))


if __name__ == "__main__":
    unittest.main()

python/save_to_db.py

"""Save a class list's marks into the results database from Chapter 4."""
import sys

import psycopg

from report import read_class

EXAM_NAME = "Mock examination"
EXAM_YEAR = 2026


def save(conn, subjects, students):
    """Add the exam, subjects, students and marks. Returns the exam's id."""
    with conn.cursor() as cur:
        cur.execute(
            """INSERT INTO exams (name, year) VALUES (%s, %s)
               ON CONFLICT (name, year) DO UPDATE SET name = EXCLUDED.name
               RETURNING id""",
            (EXAM_NAME, EXAM_YEAR),
        )
        exam_id = cur.fetchone()[0]

        subject_ids = {}
        for subject in subjects:
            cur.execute(
                """INSERT INTO subjects (name) VALUES (%s)
                   ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
                   RETURNING id""",
                (subject,),
            )
            subject_ids[subject] = cur.fetchone()[0]

        for student in students:
            cur.execute(
                """INSERT INTO students (admission_no, name, sex) VALUES (%s, %s, %s)
                   ON CONFLICT (admission_no) DO UPDATE SET name = EXCLUDED.name, sex = EXCLUDED.sex
                   RETURNING id""",
                (student["admission_no"], student["name"], student["sex"]),
            )
            student_id = cur.fetchone()[0]
            for subject, mark in student["marks"].items():
                cur.execute(
                    """INSERT INTO marks (exam_id, student_id, subject_id, mark) VALUES (%s, %s, %s, %s)
                       ON CONFLICT (exam_id, student_id, subject_id) DO UPDATE SET mark = EXCLUDED.mark""",
                    (exam_id, student_id, subject_ids[subject], mark),
                )
    return exam_id


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "class_list.csv"
    subjects, students = read_class(path)

    # The user name and password come from the environment (PGUSER and
    # PGPASSWORD), never from the code.
    with psycopg.connect(host="localhost", dbname="mwangaza") as conn:
        exam_id = save(conn, subjects, students)
        top = conn.execute(
            """SELECT position, name, average, grade FROM student_results
               WHERE exam_id = %s ORDER BY position LIMIT 3""",
            (exam_id,),
        ).fetchall()

    print(f"Saved {len(students)} students and {len(subjects)} subjects for the {EXAM_NAME}, {EXAM_YEAR}.")
    print("Top three:")
    for position, name, average, grade in top:
        print(f"  {position}. {name}, average {average}, grade {grade}")


if __name__ == "__main__":
    main()

ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name looks as if it changes nothing — and it doesn’t. It is there so that RETURNING id gives back the id of a subject or student that already existed, as well as of a new one.

Try it yourself

  1. Add an eleventh student to class_list.csv in Excel, save it as CSV UTF-8, and run the report again. Then type a mark of 105 and check the message names the right line.
  2. Add a count of students in each division, separately for girls and boys — a table head teachers are often asked for.
  3. Write a test for parse_mark that checks a blank cell gives None and "7O" raises a ValueError. (Hint: look up assertRaises.)
  4. Change report.py so it also prints the average mark for each subject, highest first.
  5. Add a divisions view to the database, or a column to results.csv, so the web page can show divisions instead of positions by average.

Key terms

EnglishKiswahiliMeaning
TerminalKituo cha amriA window where you type commands to the computer
ModuleModuliA Python file whose functions other files can import
ListOrodhaValues kept in order
DictionaryKamusiValues kept under names, called keys
LoopKitanziCode that repeats for each item
ExceptionHitilafuAn error raised while a program runs, which can be caught
TestJaribioCode that checks other code gives the right answers
LibraryMaktabaReady-made code you install and use, such as psycopg
Environment variableKigezo cha mazingiraA setting given to a program by the terminal, not written in the code
DivisionDarajaThe overall Form 4 result, worked out from points
PointsPointiThe value of a grade: A is 1, F is 5
CSV fileFaili la CSVA spreadsheet saved as plain text, with commas between cells

Check your understanding

  1. Rewrite if (mark >= 75) { return "A"; } from JavaScript in Python.
  2. A student’s grades are A, A, B, B, C, C, C, D, F. What are their points from the best seven, and what is their division?
  3. Why does Elia Kimaro, with an average of 54.1, come below Baraka Mushi, with 53.1?
  4. Every value csv.DictReader reads is a string. Why does that matter for marks, and which line of parse_mark deals with it?
  5. What does raise do in parse_mark, and where is that error finally caught?
  6. Why does report.py end with if __name__ == "__main__":?
  7. What is wrong with cur.execute(f"SELECT * FROM students WHERE name = '{name}'"), and how should it be written?
  8. Why is the database password in PGPASSWORD and not written in save_to_db.py?
  9. save_to_db.py is run twice by mistake. Why are the marks not doubled?
  10. In the tests, why check 17 and 18 points rather than, say, 10 and 30?

Answers

  1. if mark >= 75: followed, on the next line and indented, by return "A".
  2. Points 1 + 1 + 2 + 2 + 3 + 3 + 3 = 15 — the D and F are not counted — so Division I.
  3. Divisions count the best seven subjects, not the average. Elia missed Geography, so his best seven are all C grades: 21 points. Baraka’s best seven include a B: 20 points.
  4. Arithmetic on text goes wrong: "81" + 5 is an error, and text compares differently from numbers. int(text) turns the text into a number — and refuses anything that is not a whole number.
  5. It stops parse_mark and sends a ValueError back up through read_class to main, where except (OSError, ValueError) catches it and prints the message.
  6. So that main() runs only when the file is run directly. When another file imports report, its functions can be used without a report being printed.
  7. The name becomes part of the SQL, so a name containing SQL would be run: SQL injection. Write cur.execute("SELECT * FROM students WHERE name = %s", (name,)).
  8. Code is copied, shared and published. A password in an environment variable stays on the computer where it was typed and is never saved in the project.
  9. Every insert uses ON CONFLICT ... DO UPDATE: a row that already exists is updated, not added again.
  10. Mistakes happen at boundaries — writing < instead of <=, for example. 17 and 18 are exactly where Division I ends and Division II begins; 10 and 30 would pass even with that mistake.
Chapter 6

C++ essentials

Classes, inheritance and the standard library, for college courses.

Many diploma and university computing courses in Tanzania teach programming in C++, and its ideas — classes, inheritance, careful use of memory, a compiler that checks your work before it runs — appear in every serious language. This chapter is written for those courses. You will rebuild the Form 4 results calculation from Chapter 5 in C++: grades and divisions in their own files, a Student class that refuses bad marks, people of different kinds sharing a common base class, and the standard library doing the sorting and counting.

By the end of this chapter you will be able to:

  • install a C++ compiler and turn source code into a program;
  • explain how C++‘s types differ from JavaScript’s and Python’s;
  • split a program into header and source files;
  • use std::vector, std::map, std::sort and std::accumulate;
  • write classes with private data, constructors and const member functions;
  • use inheritance and virtual functions so different classes share one interface;
  • throw and catch exceptions;
  • read the compiler’s error messages and fix what they point to.

The files for this chapter go in the cpp folder of your results-system project. This chapter assumes you have worked through Chapter 5, and moves faster than the earlier ones.

6.1 A compiled language

JavaScript and Python are run by another program — the browser, or python — which reads your code line by line as it goes. C++ is different. A compiler reads all of your code first, checks it, and translates it into a program that the computer’s processor runs directly.

 grades.cpp ─┐
 student.cpp ├─►  compiler (g++)  ─►  results.exe  ─►  run it
 main.cpp ───┘     checks types,        a program
                   translates           on its own

This has three consequences you will notice at once:

  • Mistakes are found early. Use a number where text is expected, or call a function that does not exist, and the compiler refuses before the program ever runs.
  • Programs are fast. There is nothing in between your program and the processor. This is why C++ is used for games, phones, cars, databases — PostgreSQL itself is written in C — and anywhere speed matters.
  • There is a step between writing and running. Every change must be compiled again.

6.2 Setting up

On Windows, the easiest free compiler to install is g++ from MSYS2:

  1. Download the installer from msys2.org and run it, accepting the defaults.
  2. In the MSYS2 UCRT64 window that opens at the end, install the compiler:
pacman -S mingw-w64-ucrt-x86_64-gcc
  1. Add the compiler’s folder to your PATH, so any terminal can find it: press Windows, type environment variables, open Edit the system environment variables → Environment Variables, select Path under your user variables, click Edit → New, and add C:\msys64\ucrt64\bin.
  2. Close and reopen VS Code, open a terminal, and check:
g++ --version

In VS Code, also install the C/C++ extension from Microsoft. If you cannot install anything — on a shared college computer, for example — free websites such as onlinegdb.com compile and run C++ in the browser. They are fine for practice; keep your own copy of anything important.

The first program

Create hello.cpp:

#include <iostream>
#include <string>

int main() {
    std::string name = "Mwl. Grace Mushi";
    int students = 40;
    std::cout << "Habari, " << name << "! You have " << students << " students.\n";
    std::cout << "7 / 2 = " << 7 / 2 << '\n';
    std::cout << "7.0 / 2 = " << 7.0 / 2 << '\n';
    return 0;
}

Compile it, then run the program the compiler made:

g++ -std=c++17 -Wall -o hello hello.cpp
.\hello
Habari, Mwl. Grace Mushi! You have 40 students.
7 / 2 = 3
7.0 / 2 = 3.5
  • #include brings in parts of the standard library: <iostream> for printing, <string> for text.
  • Every C++ program starts at the function main. Returning 0 tells the computer it finished successfully.
  • std::cout << ... prints. The << arrows send each value to the screen, one after another. '\n' starts a new line.
  • std:: means the name comes from the standard library’s namespace, called std.
  • In the compile command, -std=c++17 chooses the 2017 version of the language, -Wall turns on warnings about suspicious code, and -o hello names the program. On Linux and macOS, run it with ./hello.

Look at the second and third lines of output. 7 / 2 is 3: when both numbers are whole numbers, C++ does integer division and throws the remainder away. With one decimal number, 7.0 / 2 is 3.5. Averages computed with whole numbers will be wrong unless you plan for this.

6.3 Types

In C++, every variable has a type that is fixed when it is created and checked by the compiler.

C++HoldsJavaScriptPython
intWhole numbersnumberint
doubleDecimalsnumberfloat
charOne character, in single quotes: 'A'stringstr
booltrue or falsebooleanbool
std::stringText, in double quotes: "Amina"stringstr
std::vector<int>A list of intsarraylist
std::map<std::string, int>Values under keys, kept in orderobjectdict

const before a type means the value can never change after it is set — the compiler enforces it. Use it whenever you can; it tells the reader, and the compiler, what will stay fixed.

To turn a whole number into a double before dividing, use static_cast<double>(total) / count. You will see exactly that in Student::average later.

6.4 Header and source files

A C++ program is usually split across several files. Each part comes in two halves:

  • a header (.h) that declares what exists — the names and types of the functions, without their bodies;
  • a source file (.cpp) that defines them — the bodies that do the work.

Other files #include the header, so they know what they can call. Here is the header for grades, grades.h:

// Grades, points and divisions for Form 4 results.
#pragma once

#include <string>
#include <vector>

namespace results {

char gradeFor(int mark);
int pointsFor(char grade);

struct Division {
    int points;        // points of the best seven subjects; 0 if incomplete
    std::string name;  // "I" to "IV", "0", or "INC"
};

Division divisionFor(const std::vector<char>& grades);

}  // namespace results
  • #pragma once stops the header being included twice in the same file, which would be an error.
  • namespace results { ... } puts these names in a group of their own. Outside, they are written results::gradeFor, so they can never clash with another library’s gradeFor.
  • A struct groups values under one name. Division returns two answers — the points and the division’s name — as one value, like the pair Python returned in Chapter 5.
  • const std::vector<char>& grades is a reference to a list that the function promises not to change. Without the &, C++ would make a complete copy of the list every time the function is called.

The definitions are in grades.cpp. The divisions follow the rule explained in Chapter 5 — the best seven subjects, with A worth 1 point and F worth 5:

int pointsFor(char grade) {
    switch (grade) {
        case 'A': return 1;
        case 'B': return 2;
        case 'C': return 3;
        case 'D': return 4;
        case 'F': return 5;
    }
    throw std::invalid_argument(std::string("not a grade: ") + grade);
}

switch picks the case matching the value — a neat way to write a chain of if statements that all test the same variable. If no case matches, the function reaches throw, which reports an error instead of returning a made-up number.

Division divisionFor(const std::vector<char>& grades) {
    const std::size_t best = 7;
    if (grades.size() < best) return {0, "INC"};

    std::vector<int> points;
    for (char grade : grades) points.push_back(pointsFor(grade));
    std::sort(points.begin(), points.end());
    int total = std::accumulate(points.begin(), points.begin() + best, 0);

    if (total <= 17) return {total, "I"};
    if (total <= 21) return {total, "II"};
    if (total <= 25) return {total, "III"};
    if (total <= 33) return {total, "IV"};
    return {total, "0"};
}

This is where the standard library earns its place:

  • for (char grade : grades) is a range-based for loop: each grade in turn, like Python’s for.
  • push_back adds an item to the end of a vector.
  • std::sort(points.begin(), points.end()) sorts the whole vector, smallest first. Standard algorithms work on a range, given as a beginning and an end.
  • std::accumulate(points.begin(), points.begin() + best, 0) adds up the first seven, starting from 0. Python’s sum(sorted(...)[:7]) became one line here too.
  • return {total, "I"} builds a Division from its two values.

6.5 Classes

A class bundles data with the functions that work on it, and decides who may touch the data. Here is the student, in student.h:

class Student : public Person {
public:
    Student(std::string name, std::string admissionNo);

    void setMark(const std::string& subject, int mark);
    double average() const;
    results::Division division() const;
    std::string describe() const override;

private:
    std::string admissionNo_;
    std::map<std::string, int> marks_;  // subject -> mark
};

Ignore : public Person and override for a moment; they are the subject of section 6.6.

  • Everything under public: can be used by any code. Everything under private: can only be used by the class’s own functions. The trailing underscore in marks_ is a common way to mark private data.
  • The constructor — the function with the same name as the class — runs when a Student is created, and sets it up.
  • A function marked const promises not to change the object. average() only reads marks, so it is const; setMark changes them, so it is not.

Why make the marks private? Because then the only way to change a mark is setMark, and setMark checks it:

void Student::setMark(const std::string& subject, int mark) {
    if (mark < 0 || mark > 100) {
        throw std::invalid_argument(name() + ", " + subject + ": " + std::to_string(mark) +
                                    " is not between 0 and 100");
    }
    marks_[subject] = mark;
}

No part of the program, however large it grows, can put a mark of 147 into a student. If someone tries to reach past the check and write s.marks_["Biology"] = 147; directly, the compiler stops them:

error: 'std::map<std::__cxx11::basic_string<char>, int> Student::marks_' is private within this context

This idea — data kept private, and changed only through functions that protect it — is called encapsulation. It is the same reason the database in Chapter 4 has CHECK constraints: put the rule in one place that cannot be avoided.

Student:: in front of the name says that this setMark belongs to the Student class. std::to_string turns a number into text, and marks_[subject] = mark stores the mark in the map under the subject’s name, replacing any mark already there.

The constructor uses a special form, the member initialiser list after the colon, to set up the base class and the data before the body runs:

Student::Student(std::string name, std::string admissionNo)
    : Person(std::move(name)), admissionNo_(std::move(admissionNo)) {}

std::move hands the text over instead of copying it — a small speed-up that you will see in most modern C++.

The average needs care with integer division, from section 6.2:

double Student::average() const {
    if (marks_.empty()) return 0.0;
    int total = 0;
    for (const auto& [subject, mark] : marks_) total += mark;
    return static_cast<double>(total) / marks_.size();
}

const auto& [subject, mark] takes each entry of the map apart into its key and value, like Python’s for subject, mark in marks.items(). auto lets the compiler work out the type itself.

6.6 Inheritance and virtual functions

A school has students and teachers. Both have names; each describes themselves differently. Inheritance lets them share what is common. The base class, in person.h:

class Person {
public:
    explicit Person(std::string name) : name_(std::move(name)) {}
    virtual ~Person() = default;

    const std::string& name() const { return name_; }

    // Every kind of person describes themselves in their own way.
    virtual std::string describe() const = 0;

private:
    std::string name_;
};

And a teacher is a kind of person:

class Teacher : public Person {
public:
    Teacher(std::string name, std::string subject)
        : Person(std::move(name)), subject_(std::move(subject)) {}

    std::string describe() const override {
        return name() + " teaches " + subject_;
    }

private:
    std::string subject_;
};
  • class Teacher : public Person means a Teacher is a Person: it has everything a person has — here, name() — plus its own subject. Student is declared the same way.
  • virtual marks a function that each derived class may provide its own version of. When the program calls describe() on a Person, C++ runs the version belonging to the real object, whether it is a teacher or a student. This is polymorphism: one call, many forms.
  • = 0 makes describe a pure virtual function: Person has no version of its own, so every derived class must write one. A class with a pure virtual function is abstract — you cannot create a plain Person, only teachers and students. Try Person p("Amina"); and the compiler says:
error: cannot declare variable 'p' to be of abstract type 'Person'
  • override says “this replaces a virtual function from the base class”. If you misspell the name, or the parameters do not match, the compiler tells you, instead of quietly creating a second, unrelated function.
  • virtual ~Person() = default; is a virtual destructor. Any class meant to be used through a base-class pointer needs one, so that deleting the object cleans up the whole of it.
  • explicit stops C++ from silently turning a plain string into a Person where you did not ask for one.

The payoff comes in main.cpp, where one list holds both kinds:

    std::vector<std::unique_ptr<Person>> people;
    people.push_back(std::make_unique<Teacher>("Mwl. Grace Mushi", "Biology"));
    people.push_back(std::make_unique<Student>(students.front()));

    std::cout << '\n';
    for (const auto& person : people) std::cout << person->describe() << '\n';

A std::unique_ptr<Person> owns an object of any class derived from Person, and deletes it automatically when the list goes away — no memory is leaked, and you never write delete yourself. -> calls a function through a pointer. The loop does not know or care which kind of person it has; each describes itself correctly.

6.7 Exceptions

When something goes wrong that the current function cannot fix, it throws an exception, as setMark does. Code further up that knows what to do catches it:

    try {
        students.front().setMark("Biology", 147);
    } catch (const std::invalid_argument& error) {
        std::cout << "\nRefused: " << error.what() << '\n';
    }

error.what() is the message given when the exception was thrown. If nothing catches an exception, the program stops — which is better than carrying on with a wrong mark.

6.8 The whole program

main.cpp puts it together: it gives five students their marks, sorts them best first, prints a table, counts the divisions, shows a bad mark being refused, and lets a teacher and a student describe themselves. Sorting uses a lambda — a small function written where it is needed:

    std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
        if (rankOf(a) != rankOf(b)) return rankOf(a) < rankOf(b);
        return a.average() > b.average();
    });

The lambda answers one question for std::sort: should a come before b? Fewer points first; if the points are equal, the higher average first. rankOf treats an incomplete result as worse than any division.

Counting uses a map: ++count[d.name]; adds one to the count for that division, and a name not yet in the map starts at zero.

A program made of several files is compiled by naming all the .cpp files. Headers are not listed; they are included by the .cpp files that need them:

g++ -std=c++17 -Wall -Wextra -o results main.cpp grades.cpp student.cpp
.\results
No. Name            Average  Points  Division
1   Neema Kileo        89.2       7  I
2   Amina Juma         79.2       7  I
3   Rehema Mollel      65.8      15  I
4   Baraka Mushi       53.1      20  II
5   Juma Hassan        38.6      26  IV

Divisions: I=3 II=1 IV=1

Refused: Neema Kileo, Biology: 147 is not between 0 and 100

Mwl. Grace Mushi teaches Biology
Neema Kileo (MSS/2023/003), 9 subjects

These five students have the same averages, points and divisions as in the Python report in Chapter 5. Two programs in two languages agreeing is good evidence that both are right.

-Wextra turns on even more warnings than -Wall. This program compiles with none. Aim for that in your own work: a warning is the compiler pointing at something that is probably a mistake.

6.9 Reading compiler errors

The compiler’s messages look alarming at first, but they follow a pattern: file, line, column, then what is wrong. Always fix the first error first — one mistake often causes a cascade of confusing errors after it, which disappear when the first is fixed.

The compiler saysWhat it means
main.cpp:4:5: error: expected ',' or ';' before 'return'A semicolon is missing at the end of the line before line 4
error: 'cout' was not declared in this scope; did you mean 'std::cout'?The std:: is missing, or the #include is
error: cannot declare variable 'p' to be of abstract type 'Person'You tried to create an object of a class with a pure virtual function
error: ... is private within this contextCode outside a class tried to use its private data
undefined reference to 'Student::division() const'This comes from the linker, the last step: a function was declared but its .cpp file was not included in the compile command

The last one is worth understanding. Compiling has two stages: each .cpp file is compiled on its own, and then the linker joins the pieces into one program. If you forget student.cpp in the command, every file still compiles — the header promised Student::division exists — but the linker cannot find its body.

6.10 The complete files

The cpp folder holds six files. Compile them with the command in section 6.8.

cpp/grades.h

// Grades, points and divisions for Form 4 results.
#pragma once

#include <string>
#include <vector>

namespace results {

char gradeFor(int mark);
int pointsFor(char grade);

struct Division {
    int points;        // points of the best seven subjects; 0 if incomplete
    std::string name;  // "I" to "IV", "0", or "INC"
};

Division divisionFor(const std::vector<char>& grades);

}  // namespace results

cpp/grades.cpp

#include "grades.h"

#include <algorithm>
#include <numeric>
#include <stdexcept>

namespace results {

char gradeFor(int mark) {
    if (mark < 0 || mark > 100) {
        throw std::invalid_argument("a mark must be between 0 and 100");
    }
    if (mark >= 75) return 'A';
    if (mark >= 65) return 'B';
    if (mark >= 45) return 'C';
    if (mark >= 30) return 'D';
    return 'F';
}

int pointsFor(char grade) {
    switch (grade) {
        case 'A': return 1;
        case 'B': return 2;
        case 'C': return 3;
        case 'D': return 4;
        case 'F': return 5;
    }
    throw std::invalid_argument(std::string("not a grade: ") + grade);
}

Division divisionFor(const std::vector<char>& grades) {
    const std::size_t best = 7;
    if (grades.size() < best) return {0, "INC"};

    std::vector<int> points;
    for (char grade : grades) points.push_back(pointsFor(grade));
    std::sort(points.begin(), points.end());
    int total = std::accumulate(points.begin(), points.begin() + best, 0);

    if (total <= 17) return {total, "I"};
    if (total <= 21) return {total, "II"};
    if (total <= 25) return {total, "III"};
    if (total <= 33) return {total, "IV"};
    return {total, "0"};
}

}  // namespace results

cpp/person.h

// People in a school: what every person has, and how each kind differs.
#pragma once

#include <string>
#include <utility>

class Person {
public:
    explicit Person(std::string name) : name_(std::move(name)) {}
    virtual ~Person() = default;

    const std::string& name() const { return name_; }

    // Every kind of person describes themselves in their own way.
    virtual std::string describe() const = 0;

private:
    std::string name_;
};

class Teacher : public Person {
public:
    Teacher(std::string name, std::string subject)
        : Person(std::move(name)), subject_(std::move(subject)) {}

    std::string describe() const override {
        return name() + " teaches " + subject_;
    }

private:
    std::string subject_;
};

cpp/student.h

// A student and their marks for one examination.
#pragma once

#include <map>
#include <string>

#include "grades.h"
#include "person.h"

class Student : public Person {
public:
    Student(std::string name, std::string admissionNo);

    void setMark(const std::string& subject, int mark);
    double average() const;
    results::Division division() const;
    std::string describe() const override;

private:
    std::string admissionNo_;
    std::map<std::string, int> marks_;  // subject -> mark
};

cpp/student.cpp

#include "student.h"

#include <stdexcept>
#include <utility>
#include <vector>

Student::Student(std::string name, std::string admissionNo)
    : Person(std::move(name)), admissionNo_(std::move(admissionNo)) {}

void Student::setMark(const std::string& subject, int mark) {
    if (mark < 0 || mark > 100) {
        throw std::invalid_argument(name() + ", " + subject + ": " + std::to_string(mark) +
                                    " is not between 0 and 100");
    }
    marks_[subject] = mark;
}

double Student::average() const {
    if (marks_.empty()) return 0.0;
    int total = 0;
    for (const auto& [subject, mark] : marks_) total += mark;
    return static_cast<double>(total) / marks_.size();
}

results::Division Student::division() const {
    std::vector<char> grades;
    for (const auto& [subject, mark] : marks_) grades.push_back(results::gradeFor(mark));
    return results::divisionFor(grades);
}

std::string Student::describe() const {
    return name() + " (" + admissionNo_ + "), " + std::to_string(marks_.size()) + " subjects";
}

cpp/main.cpp

// Form 4 results: classes, inheritance and the standard library.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

#include "person.h"
#include "student.h"

// Lower is better: points of the best seven, with incomplete results last.
int rankOf(const Student& student) {
    results::Division d = student.division();
    return d.name == "INC" ? 100 : d.points;
}

int main() {
    const std::vector<std::string> subjects = {"Civics",  "History",     "Geography", "Kiswahili", "English",
                                               "Biology", "Mathematics", "Chemistry", "Physics"};
    const std::vector<std::vector<int>> marks = {
        {82, 76, 79, 88, 85, 81, 78, 74, 70},
        {58, 49, 61, 66, 61, 47, 52, 44, 40},
        {90, 85, 88, 92, 88, 94, 91, 86, 89},
        {38, 41, 35, 52, 49, 38, 34, 29, 31},
        {68, 70, 63, 75, 72, 59, 66, 58, 61},
    };
    std::vector<Student> students = {
        {"Amina Juma", "MSS/2023/001"},  {"Baraka Mushi", "MSS/2023/002"},
        {"Neema Kileo", "MSS/2023/003"}, {"Juma Hassan", "MSS/2023/004"},
        {"Rehema Mollel", "MSS/2023/005"},
    };

    for (std::size_t i = 0; i < students.size(); ++i) {
        for (std::size_t j = 0; j < subjects.size(); ++j) {
            students[i].setMark(subjects[j], marks[i][j]);
        }
    }

    // Best first: fewest points, then the higher average.
    std::sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
        if (rankOf(a) != rankOf(b)) return rankOf(a) < rankOf(b);
        return a.average() > b.average();
    });

    std::cout << std::left << std::setw(4) << "No." << std::setw(15) << "Name" << std::right
              << std::setw(8) << "Average" << std::setw(8) << "Points" << "  Division\n";

    std::map<std::string, int> count;
    int number = 1;
    for (const Student& s : students) {
        results::Division d = s.division();
        std::cout << std::left << std::setw(4) << number++ << std::setw(15) << s.name() << std::right
                  << std::fixed << std::setprecision(1) << std::setw(8) << s.average()
                  << std::setw(8) << d.points << "  " << d.name << '\n';
        ++count[d.name];
    }

    std::cout << "\nDivisions:";
    for (const auto& [name, n] : count) std::cout << ' ' << name << '=' << n;
    std::cout << '\n';

    // A mistake in the data is refused, not quietly accepted.
    try {
        students.front().setMark("Biology", 147);
    } catch (const std::invalid_argument& error) {
        std::cout << "\nRefused: " << error.what() << '\n';
    }

    // A teacher and a student are both people, so one list can hold both.
    std::vector<std::unique_ptr<Person>> people;
    people.push_back(std::make_unique<Teacher>("Mwl. Grace Mushi", "Biology"));
    people.push_back(std::make_unique<Student>(students.front()));

    std::cout << '\n';
    for (const auto& person : people) std::cout << person->describe() << '\n';

    return 0;
}

std::setw(15) sets the width of the next value printed, and std::left and std::right line it up, as < and > did in Python’s f-strings. std::fixed with std::setprecision(1) prints one decimal place.

Try it yourself

  1. Add Elia Kimaro from Chapter 5, who missed Geography: give him every mark except that one. Check that his points and division match the Python report.
  2. Add a HeadTeacher class that inherits from Teacher and describes themselves differently. Does people need any change to hold one?
  3. Read the class list from class_list.csv instead of the marks written in main. You will need <fstream>, std::getline and a way to split a line at commas.
  4. Write a small test program, test_grades.cpp, that uses assert from <cassert> to check the grade and division boundaries tested in Chapter 5.
  5. Add a function to Student that returns its best subject. Should it be const?

Key terms

EnglishKiswahiliMeaning
CompilerKikusanyajiA program that checks source code and turns it into a program the computer runs
Source codeMsimbo chanzoThe code you write, before it is compiled
Header fileFaili kichwaA .h file that declares what other files can use
TypeAinaThe kind of value a variable holds, such as int or std::string
ClassDarasaA bundle of data and the functions that work on it
ObjectKituOne value made from a class, such as one student
ConstructorKijenziThe function that sets up a new object
InheritanceUrithiA class taking on everything another class has, and adding to it
EncapsulationUfichajiKeeping data private and changing it only through checked functions
PolymorphismUpolimofuOne call running different code depending on the real object
ExceptionHitilafuAn error that is thrown and can be caught further up
LinkerKiunganishiThe last step of compiling, joining all the pieces into one program

Check your understanding

  1. What does a compiler do that Python and the browser do not do before your program runs?
  2. Why does 7 / 2 give 3 in C++, and how do you get 3.5?
  3. What is the difference between grades.h and grades.cpp?
  4. Why is the parameter of divisionFor written const std::vector<char>& grades and not std::vector<char> grades?
  5. Why are a student’s marks private, and what would go wrong if they were public?
  6. Why is average() a const function, but not setMark?
  7. What does = 0 after virtual std::string describe() const mean, and what does it stop you doing?
  8. In the loop over people, how does C++ know whether to run Teacher::describe or Student::describe?
  9. You compile with g++ -o results main.cpp grades.cpp and get undefined reference to 'Student::division() const'. What is wrong?
  10. The compiler reports twelve errors. Which do you fix first, and why?

Answers

  1. It reads and checks the whole program before anything runs — types, names, function calls — and translates it into a program the processor runs directly.
  2. Both numbers are int, so C++ does integer division and drops the remainder. Make one of them a double: 7.0 / 2, or static_cast<double>(7) / 2.
  3. The header declares the functions and types — what exists. The source file defines them — the bodies that do the work. Other files include the header.
  4. The & passes a reference, so the vector is not copied each time; const promises the function will not change it.
  5. So that the only way to change a mark is setMark, which checks it. If they were public, any code could store a mark of 147 and every result would be wrong.
  6. average() only reads the marks, so it promises not to change the object. setMark changes the marks, so it cannot be const.
  7. It makes describe pure virtual: Person has no version of its own and every derived class must write one. You cannot create a plain Person object.
  8. describe is virtual, so C++ looks at the real type of each object when the program runs and calls that class’s version.
  9. student.cpp is missing from the command, so the linker cannot find the bodies of Student’s functions. Add it: g++ -o results main.cpp grades.cpp student.cpp.
  10. The first. A single mistake, such as a missing semicolon, often causes many confusing errors after it, which disappear once it is fixed.
Chapter 7

Staying safe online

Protect your devices, your accounts, your money and your work.

Every lesson in this handbook ends with something you build: a web page, a database, a script. This chapter is about protecting all of it — and protecting you. Most attacks on ordinary people are not clever. They succeed because someone clicked too quickly, reused a password, or trusted a message that looked official. The good news is that a few steady habits stop almost all of them.

By the end of this chapter you will be able to:

  • recognise the common kinds of harmful software and how they reach a computer;
  • spot a scam message, a fake website and a dangerous link before you act on them;
  • choose passwords that are strong and still easy to remember;
  • protect your mobile money, your email and your school accounts;
  • keep backups so that no single mistake can destroy your work;
  • know exactly what to do in the first hour after something goes wrong.

7.1 Why this matters

Think about what lives on your phone and computer: your mobile money, your WhatsApp conversations, photos, school work, and the accounts you use to sign in to everything else. For a teacher it may also hold students’ names, marks and parents’ phone numbers. Losing it is bad. Having it stolen and used against you — or against your students — is worse.

Attackers rarely choose a person because they are important. They send the same trick to thousands of people and wait for the few who fall for it. That is why you, not your antivirus, are the most important part of your security.

7.2 Harmful software

Malware (short for “malicious software”) is any program written to harm you, spy on you or take control of your device. You will hear many names for it. The table shows the ones worth knowing.

TypeWhat it doesA typical example
VirusAttaches itself to files and spreads when they are opened or copiedAn infected document passed from computer to computer
WormSpreads across a network by itself, with no one opening anythingA whole computer lab slowing down in one afternoon
TrojanPretends to be something useful while doing harm in the backgroundA “free” copy of expensive software that also installs spyware
RansomwareLocks your files and demands payment to unlock themEvery document renamed with a strange ending and a ransom note on the desktop
SpywareWatches what you type and do, then sends it to someone elsePasswords stolen while you sign in to your email
AdwareFloods you with advertisements, often in new browser tabsPop-ups that appear even when no browser is open

How it gets in

Harmful software almost never “just appears”. It arrives through a door that someone opened. The most common doors are:

  1. Flash drives. A USB flash drive that has been in an infected computer — often at a printing or internet café — carries the infection to the next computer it is plugged into. The well-known “shortcut virus”, which hides your folders and replaces them with shortcuts, spreads exactly this way.
  2. Cracked software. Programs downloaded with a “crack”, “patch” or “keygen” to avoid paying are one of the most reliable ways to get infected. The person who removed the licence check can add anything else they like.
  3. Attachments and links. A file or link in an email, SMS or WhatsApp message that you were not expecting.
  4. Fake updates and fake apps. A website announcing that “your Flash Player is out of date”, or an app installed from outside the Google Play Store or Apple App Store.
  5. Old, unpatched software. Software that has not been updated for months has known weaknesses that attackers already know how to use.

Signs that something is wrong

  • The computer or phone has suddenly become much slower, or the fan runs loudly while you are doing nothing.
  • Folders on a flash drive have turned into shortcuts, or files have disappeared.
  • New toolbars, home pages or programs appear that you did not install.
  • Friends receive messages from you that you never sent.
  • Your antivirus has been switched off and will not turn back on.
  • Mobile data or airtime runs out much faster than usual.

One sign alone may have an innocent explanation. Several together deserve attention.

7.3 Your first line of defence

Keep everything updated

Updates are not only new features. Most of them close security holes that attackers are actively using. Turn on automatic updates for Windows, for your phone, and for the apps you use most — especially your browser.

Use the antivirus you already have

Windows 10 and 11 include Windows Security (also called Microsoft Defender). It is free, it updates itself, and for most people it is enough. Make sure it is on:

  1. Open Start and type Windows Security.
  2. Choose Virus & threat protection.
  3. Check that Real-time protection is on, then run a Quick scan.

Run a Full scan whenever you suspect a problem. Do not install two antivirus programs at the same time: they fight each other and make the computer slower without making it safer.

Handle flash drives with care

  • Scan a flash drive before opening its files: right-click it in File Explorer and choose Scan with Microsoft Defender.
  • Never run a program (a file ending in .exe, .bat or .vbs) that you find on someone else’s flash drive.
  • If your folders have turned into shortcuts, do not click the shortcuts. Scan the drive first — the shortcuts usually run the virus again.

Install software only from trusted places

Download programs from the maker’s own website or from the official app store. If a paid program is too expensive, look for a free alternative instead of a cracked copy: LibreOffice instead of a pirated office suite, GIMP instead of a pirated photo editor, VS Code for programming.

7.4 Scams, phishing and fake messages

Phishing is a trick that makes you hand over information or money by pretending to be someone you trust: a bank, your mobile network, NECTA, your school, or a friend. It arrives by SMS, WhatsApp, email and phone call.

The scams you will meet most often

“Send it to this number.” You receive an SMS that seems to continue an earlier conversation: “Ile hela usitume kwenye namba ile, tuma kwenye namba hii…” (“Don’t send that money to that number, send it to this one…”). There was no earlier conversation. It is sent to thousands of people in the hope that one of them is about to send money to someone.

“I sent you money by mistake.” You receive a message that looks like a mobile money confirmation, followed by a polite request to send the money back. Check your real balance first. In most cases no money ever arrived — the “confirmation” was an ordinary SMS typed to look official.

“Your account will be closed.” A message claims to come from your network or bank and asks you to confirm your PIN, or to read out a code that “we have just sent you”. No genuine company will ever ask for your PIN or a verification code.

“You have won.” A prize, a scholarship or a job you never applied for — but first you must pay a small “registration” or “processing” fee.

Fake results and fake portals. During exam season, fake websites promise early access to results or “upgraded” grades. Results are published only by NECTA, on its official website.

How to check a link before you trust it

Attackers make web addresses that look almost right. Learn to read an address from the right-hand end of the domain:

AddressReal or fake?Why
https://www.necta.go.tz/resultsRealThe domain is necta.go.tz. Government sites in Tanzania end in .go.tz.
https://necta.go.tz.results-2026.comFakeEverything before .results-2026.com is decoration. The real domain is results-2026.com.
https://www.nectaa.go.tzFakeAn extra letter. Easy to miss when you read quickly.
https://bit.ly/3xYz…UnknownA shortened link hides where it really goes. Be careful with these from strangers.

Questions to ask before you act

  1. Was I expecting this? Unexpected messages deserve extra suspicion.
  2. Is it trying to rush me? “Within 24 hours”, “last chance” and “your account will be blocked” are pressure tactics.
  3. Is it asking for a PIN, a code, a password or money? Then stop.
  4. Can I check it another way? Call the person on the number you already have for them, or open the official app or website yourself — never through the link in the message.

7.5 Passwords and accounts

Make passwords long, not complicated

A password’s strength comes mostly from its length. Four or five random words joined together are both strong and easy to remember:

PasswordStrengthWhy
Juma2007Very weakA name and a birth year are the first things an attacker tries.
P@ssw0rd!WeakSwapping letters for symbols is a trick attackers expect.
mango-river-tuesday-pianoStrongLong, random words, and still easy to type and remember.

Never reuse a password

When one website is broken into, attackers try the stolen email-and-password pairs on every other popular site. If you used the same password for your email, every account linked to that email is now at risk. Use a different password for every important account.

Remembering many passwords is hard, so let a password manager do it. Your browser has one built in (Google Password Manager in Chrome, for example), and Bitwarden is a respected free option. You then remember one strong password; the manager remembers the rest.

Turn on two-step verification

Two-step verification asks for something extra after your password — usually a code sent to your phone or shown in an authenticator app. Even if someone learns your password, they cannot sign in without your phone. Turn it on for your email first, because your email is the key that resets every other account. Then do the same for WhatsApp (under Settings → Account → Two-step verification), Facebook, Instagram and your school accounts.

Protect your SIM card

Your phone number receives your mobile money messages and your verification codes, which makes it valuable. In a SIM swap, a criminal persuades a shop to transfer your number to a new SIM card. If your phone suddenly loses network for no reason, contact your network provider immediately through its official channels. Set a PIN on your SIM card in your phone’s security settings, and never share copies of your ID documents with strangers.

7.6 Backups: your safety net

Backups turn a disaster into an inconvenience. If your laptop is stolen, your flash drive is lost or ransomware locks your files, a recent backup means you lose an afternoon instead of a year of work.

Follow the 3-2-1 rule:

  • keep 3 copies of anything important — the original plus two backups;
  • on 2 different kinds of storage — for example your laptop and an external drive;
  • with 1 copy somewhere else — online (Google Drive, OneDrive) or at another place.

Test your backup now and again by opening a file from it. A backup you have never tested is only a hope.

7.7 Staying private online

  • Think before you post. Photos and posts can show your school uniform, your home, your daily route and when your house is empty. Once something is shared, you cannot fully take it back.
  • Check your privacy settings. On WhatsApp, decide who can see your profile photo, “last seen” and status, and who can add you to groups.
  • Be careful on public Wi-Fi. On free Wi-Fi in a café or bus station, avoid signing in to banking or mobile money. Your mobile data is safer for anything sensitive.
  • Lock your devices. Use a PIN, pattern or fingerprint on your phone, and a password on your computer. Lock the computer when you step away: press Win + L.
  • Protect other people’s data too. Teachers handle students’ marks, photos and phone numbers. Share them only with people who need them, and never post them in public groups.

7.8 If something goes wrong

Act quickly and calmly. The first hour matters most.

If you sent money to a scammer

  1. Contact your mobile money provider or bank immediately through its official customer care, and ask them to stop or reverse the transaction.
  2. Keep the messages — do not delete them. Take screenshots.
  3. Report the case at the nearest police station. Tanzania’s Cybercrimes Act, 2015 makes online fraud a crime.

If an account has been taken over

  1. Use the service’s “forgot password” option to take it back, and choose a new password you have never used before.
  2. Sign out of all other devices from the account’s security settings.
  3. Turn on two-step verification.
  4. Warn your contacts that messages sent from the account were not from you.

If your computer is infected

  1. Disconnect it from the internet.
  2. Run a full scan with Windows Security.
  3. Change your important passwords — from a different, clean device.
  4. If the infection will not go away, back up your documents (not programs) and have the computer reinstalled by a trusted technician.

Incidents that affect many people, or organisations such as schools, can also be reported to TZ-CERT, Tanzania’s national computer emergency response team, which works under the Tanzania Communications Regulatory Authority (TCRA).

7.9 Your security checklist

Copy this list and tick each item when it is done.

  • Automatic updates are on for my computer, my phone and my browser.
  • Windows Security is on and has run a scan this month.
  • I scan flash drives before opening them.
  • I have removed any cracked software.
  • Every important account has its own password.
  • My email has two-step verification turned on.
  • WhatsApp has two-step verification turned on.
  • My phone and computer lock with a PIN or password.
  • My SIM card has a PIN.
  • I have a backup of my important files in two places, and I have tested it.

Key terms

EnglishKiswahiliMeaning
MalwareProgramu hasidiSoftware written to cause harm
Computer virusVirusi vya kompyutaMalware that spreads by attaching itself to files
AntivirusProgramu ya kuzuia virusiSoftware that finds and removes malware
UpdateSasishoA new version that fixes problems and security holes
PasswordNenosiriA secret word used to prove who you are
Two-step verificationUthibitisho wa hatua mbiliA second check, such as a code, after your password
ScamUtapeliA trick to steal money or information
HackerMdukuziA person who breaks into computers or accounts
BackupNakala rudufuAn extra copy of your files kept somewhere safe
LinkKiungoA clickable address that opens a web page

Check your understanding

  1. A friend gives you a flash drive at the internet café. What should you do before opening the files on it?
  2. Why is mango-river-tuesday-piano a stronger password than P@ssw0rd!?
  3. You receive an SMS: “Tumekutumia TZS 50,000 kimakosa. Tafadhali rudisha kwenye namba hii.” (“We sent you TZS 50,000 by mistake. Please return it to this number.”) What do you check first?
  4. Which part of https://necta.go.tz.results-2026.com is the real domain, and is the site official?
  5. Why should you turn on two-step verification for your email before any other account?
  6. Explain the 3-2-1 backup rule in your own words.
  7. Your phone suddenly shows “No service” while everyone around you has signal. What could be happening, and what should you do?

Answers

  1. Scan it with Windows Security (right-click → Scan with Microsoft Defender), and do not run any programs found on it.
  2. It is much longer. Length makes a password hard to guess, and letter-to-symbol swaps such as @ for a are tricks attackers already try.
  3. Check your real mobile money balance in the official menu or app. Do not trust the SMS — a fake confirmation is just a typed message.
  4. The real domain is results-2026.com, read from the right-hand end. It is not official; NECTA’s own site is necta.go.tz.
  5. Email is used to reset the passwords of almost every other account. Whoever controls your email can take over the rest.
  6. Keep three copies of important files, on two different kinds of storage, with one copy kept somewhere else (online or at another location).
  7. It could be a SIM swap. Contact your network provider immediately through its official channels and check your mobile money account.
Chapter 8

Git and GitHub

Keep every version of your project safe, share it, and publish it.

You have now built a whole system: a web page, a database, a Python report and a C++ program. Imagine losing it — a stolen laptop, a flash drive with the shortcut virus from Chapter 7, or one bad edit that breaks everything the night before you show it to the head teacher. Git makes sure that never costs you more than a few minutes. It keeps every version of every file you choose to save, lets you try ideas without risk, and — with GitHub — keeps a copy online, lets others work with you, and can even put your results page on the internet for free.

By the end of this chapter you will be able to:

  • install Git and tell it who you are;
  • turn your project into a repository and save versions of it with commits;
  • see exactly what changed, and undo changes you do not want;
  • keep files that should never be saved — like passwords — out of Git;
  • try ideas on a branch and merge them, including fixing a conflict;
  • put your project on GitHub, and work on it from more than one computer;
  • publish the results page as a real website with GitHub Pages.

8.1 What Git does

Without Git, most people keep versions like this:

results-final.html
results-final-2.html
results-final-REALLY-final.html
results-old-dont-delete.html

Nobody remembers which is which, or what changed between them. Git replaces that with a single folder and a history. Each time you reach a good point, you make a commit: a saved snapshot of the whole project, with a message saying what changed and why. You can look back at any commit, compare any two, and return to any one.

WordMeaning
Repository (“repo”)A project folder whose history Git keeps. The history lives in a hidden .git folder inside it
CommitOne saved snapshot of the project, with a message, an author and a date
StagingChoosing which changes go into the next commit
BranchA separate line of work, so you can try something without touching the main version
MergeBringing the work from one branch into another
RemoteA copy of the repository somewhere else — usually on GitHub
Push / pullSending your commits to the remote / bringing others’ commits from it

Git runs on your own computer and needs no internet. GitHub is a website that stores Git repositories online. They are separate: you can use Git without GitHub, but not GitHub without Git.

8.2 Installing and setting up Git

  1. Download Git for Windows from git-scm.com and run the installer. The default choices are fine; click Next until it finishes.
  2. Open a new terminal in VS Code (Ctrl + ` ) and check:
git --version
  1. Tell Git who you are. Every commit records this name and email, so use your real name and an email you check:
git config --global user.name "Grace Mushi"
git config --global user.email "grace@example.com"
git config --global init.defaultBranch main

The last line names the first branch of every new repository main, which is what GitHub expects. --global means these settings apply to every repository on this computer, so you only do this once.

8.3 Your first commit

Move into your project folder and turn it into a repository:

cd Documents\Projects\results-system
git init
Initialized empty Git repository in C:/Users/Grace/Documents/Projects/results-system/.git/

The path will be your own. Now ask Git what it sees — the command you will type more than any other:

git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	cpp/
	database/
	python/
	web/

nothing added to commit but untracked files present (use "git add" to track)

Untracked means Git can see the files but is not keeping their history yet.

What not to save

Some files should never go into Git:

  • Files your programs make, such as results.csv from Chapter 5, Python’s __pycache__ folder and the results.exe that the C++ compiler builds. They can always be made again, and saving them only creates noise.
  • Secrets — passwords, API keys, the settings file that holds them. Chapter 5 kept the database password out of the code; this keeps it out of Git.

List them in a file called .gitignore (with the dot) in the project’s top folder:

# Made by the programs: build them again, never edit them by hand
python/results.csv
python/__pycache__/
cpp/results.exe

# Secrets never go in Git
.env

Each line is a file or folder for Git to ignore; lines starting with # are comments. git status --short shows the result in one line per item:

git status --short
?? .gitignore
?? cpp/
?? database/
?? python/
?? web/

?? means untracked.

Stage and commit

Saving a commit takes two steps. git add stages changes — puts them in the next snapshot. git commit saves the snapshot with a message:

git add .
git commit -m "Results page, database, Python report and C++ program"
[main (root-commit) b63706d] Results page, database, Python report and C++ program
 18 files changed, 1112 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 cpp/grades.cpp
 create mode 100644 cpp/grades.h
 create mode 100644 cpp/main.cpp
 create mode 100644 cpp/person.h
 create mode 100644 cpp/student.cpp
 create mode 100644 cpp/student.h
 create mode 100644 database/data.sql
 create mode 100644 database/results.sql
 create mode 100644 database/schema.sql
 create mode 100644 python/class_list.csv
 create mode 100644 python/grades.py
 create mode 100644 python/report.py
 create mode 100644 python/save_to_db.py
 create mode 100644 python/test_grades.py
 create mode 100644 web/results.html
 create mode 100644 web/results.js
 create mode 100644 web/style.css

The dot in git add . means “everything in this folder”. Notice what is not in the list: results.csv, __pycache__ and results.exe were ignored, exactly as .gitignore asked.

b63706d is the start of the commit’s ID, a long code that Git calculates from the commit’s contents. Yours will be different — every commit in the world has its own. You can refer to a commit by these first seven characters.

git log --oneline lists the history, newest first:

b63706d Results page, database, Python report and C++ program

Writing good messages

A commit message is a note to the future — often to yourself in six months. Say what changed and, when it is not obvious, why:

Poor messageBetter message
updateShow the summary numbers in blue
fixed itRefuse marks above 100 in the entry form
changesCount Elia's missed paper as absent, not zero

8.4 The everyday cycle

Change something: in web/style.css, make the three summary numbers blue instead of green. Then ask Git:

git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   web/style.css

no changes added to commit (use "git add" and/or "git commit -a")

git diff shows exactly what changed, line by line:

git diff
diff --git a/web/style.css b/web/style.css
index 03d455c..1fcf263 100644
--- a/web/style.css
+++ b/web/style.css
@@ -74,7 +74,7 @@ h2 {
   display: block;
   font-size: 1.5rem;
   font-weight: bold;
-  color: #2e8b2a;
+  color: #1e6fb8;
 }
 
 /* Results table */

A line starting with - was removed and a line starting with + was added. The lines around them, with no sign, are there to show where the change is. @@ -74,7 +74,7 @@ says the change is near line 74. Read the diff before every commit: it is the last chance to notice a change you did not mean to make.

Stage and commit it:

git add web/style.css
git commit -m "Show the summary numbers in blue"
[main 8173a1c] Show the summary numbers in blue
 1 file changed, 1 insertion(+), 1 deletion(-)

That is the whole everyday cycle: change, status, diff, add, commit. Commit whenever you reach a point that works — several times a day is normal. Small commits with clear messages are easy to understand and easy to undo.

Undoing a change

You edited a file, it went wrong, and you want it back exactly as it was at the last commit:

git restore web/results.html

The file returns to its last committed version. This cannot be undone — the edits since the last commit are gone — so check git diff first to be sure.

To look at an old version without changing anything, git show 8173a1c displays that commit and its diff. Git has many more ways to undo, but restoring a file and making a new commit to fix a mistake cover nearly every situation a beginner meets.

8.5 Branches

A branch is a separate line of work. You create one to try something — a new feature, a risky change — while main stays safe. If the idea works, you merge it back; if not, you delete the branch and nothing is lost.

Create a branch and move onto it in one step:

git switch -c test-zero-mark
Switched to a new branch 'test-zero-mark'

Add a test to python/test_grades.py checking that a mark of 0 gets an F, and commit it. git commit -am stages every changed file that Git already tracks and commits it, in one command:

git commit -am "Test that a mark of 0 is an F"
[test-zero-mark bea4dd2] Test that a mark of 0 is an F
 1 file changed, 1 insertion(+)

The test passes, so bring the work into main. Switch back, then merge:

git switch main
git merge test-zero-mark
Updating 8173a1c..bea4dd2
Fast-forward
 python/test_grades.py | 1 +
 1 file changed, 1 insertion(+)

Fast-forward means main had not changed since the branch began, so Git simply moved main forward to include the new commit. The branch has done its job; delete it:

git branch -d test-zero-mark
Deleted branch test-zero-mark (was bea4dd2).

When two changes meet: conflicts

Usually Git merges changes by itself, even when both branches changed the same file. It only needs help when both changed the same lines in different ways. Suppose a branch called add-country changed the school’s address line to add “Tanzania”, while on main someone changed the post office box. Merging gives:

git merge add-country
Auto-merging web/results.html
CONFLICT (content): Merge conflict in web/results.html
Automatic merge failed; fix conflicts and then commit the result.

Nothing is lost. Git has written both versions into the file, between markers:

<<<<<<< HEAD
      <p>P.O. Box 5678, Dodoma</p>
=======
      <p>P.O. Box 1234, Dodoma, Tanzania</p>
>>>>>>> add-country

Above ======= is the version on the branch you are on (HEAD, here main); below it is the version from add-country. Your job is to decide what the line should be — here, both changes are right, so keep the new box number and the country — and delete the three marker lines:

      <p>P.O. Box 5678, Dodoma, Tanzania</p>

Then stage the file to mark the conflict as resolved, and commit:

git add web/results.html
git commit -m "Merge the new address and the country"

git log --oneline --graph draws the history, showing the two lines of work and where they joined:

*   6461290 Merge the new address and the country
|\  
| * 41734b2 Add the country to the address
* | 9d250be New post office box
|/  
* bea4dd2 Test that a mark of 0 is an F
* 8173a1c Show the summary numbers in blue
* b63706d Results page, database, Python report and C++ program

8.6 GitHub

So far, the history lives only on your computer. GitHub keeps a copy online: a backup, a way to work from several computers, and a place to work with others.

Create an account and a repository

  1. Sign up at github.com. Choose a username you would be happy to show an employer — many developers’ GitHub pages are part of their CV.
  2. Turn on two-step verification when GitHub asks (Chapter 7 explained why). GitHub requires it for anyone who publishes code.
  3. Click New (or the + at the top right → New repository). Name it results-system. Choose Private for now: only you can see it. Do not tick “Add a README file” — your repository already has a history to send.
  4. Click Create repository. GitHub shows the address of the new repository, ending in results-system.git.

Push

Tell your local repository about the one on GitHub. The remote is usually called origin:

git remote add origin https://github.com/YOUR-USERNAME/results-system.git
git push -u origin main

Replace YOUR-USERNAME with your GitHub username. The first time, Git for Windows opens a window to sign in to GitHub in your browser; after that it remembers. When the push finishes, refresh the repository’s page on GitHub: every file and every commit is there.

-u links your main to GitHub’s main, so from now on git push and git pull, with nothing after them, know where to go. The everyday cycle gains one step at the end:

git add .
git commit -m "Describe the change"
git push

Working from another computer

On a second computer — at school, say — get a full copy of the repository, with its whole history:

git clone https://github.com/YOUR-USERNAME/results-system.git

Before starting work there, and every time you return to a computer, bring in what was pushed from elsewhere:

git pull

Pull before you start, push when you stop. That habit prevents almost all conflicts when you work in more than one place.

Working with others

On a team, nobody pushes untested changes straight to main. Each person works on a branch, pushes it, and opens a pull request on GitHub: a page that shows the branch’s changes and asks for them to be merged. Teammates read the diff, comment on lines, and approve it; then it is merged on GitHub. This is how almost all professional software is built, including the projects this book is based on.

8.7 Your results page on the internet

GitHub can host a website for free with GitHub Pages, straight from your repository. Your results page from Chapter 3 is pure HTML, CSS and JavaScript, so it works as it is.

On a free GitHub account, Pages works only for public repositories, so first make sure the repository contains nothing private: the invented class from this book is fine, real marks are not. Then:

  1. On the repository’s page, open Settings → General, scroll to the Danger Zone, and choose Change visibility → Make public.
  2. Open Settings → Pages. Under Build and deployment, set Source to GitHub Actions.
  3. GitHub suggests workflows. Find Static HTML and click Configure.
  4. A file called static.yml opens in GitHub’s editor. Find the line path: '.' and change it to path: './web', so that only the web folder is published.
  5. Click Commit changes.

GitHub builds the site in about a minute; the Actions tab shows the progress. Your page is then at:

https://YOUR-USERNAME.github.io/results-system/results.html

Open it on your phone. Everything from Chapters 2 and 3 works: the table, the grade badges, entering marks, and the marks being remembered in that browser. Every time you push a change to the web folder, the site updates by itself.

The commit GitHub just made lives on GitHub, not on your computer yet. Run git pull before your next change to bring it down.

8.8 When something goes wrong

Git saysWhat it means
fatal: not a git repositoryThe terminal is not inside your project folder. Use cd to move into it
nothing to commit, working tree cleanEvery change is already committed. Good news
Author identity unknownGit does not know your name: run the git config commands in section 8.2
error: failed to push some refs … Updates were rejectedGitHub has commits you do not have yet. Run git pull, fix any conflicts, then push again
CONFLICT (content): Merge conflict in …Two changes touched the same lines. Edit the file as in section 8.5, then add and commit
error: Your local changes … would be overwrittenYou have uncommitted changes that a switch or pull would lose. Commit them first
Permission denied or a 403 error when pushingYou are signed in as someone else, or the address is wrong. Check it with git remote -v

Key terms

EnglishKiswahiliMeaning
Version controlUdhibiti wa matoleoKeeping a history of every saved version of your files
RepositoryHazinaA project whose history Git keeps
CommitKuhifadhi toleoOne saved snapshot of the project, with a message
BranchTawiA separate line of work
MergeKuunganishaBringing one branch’s work into another
ConflictMgonganoTwo changes to the same lines, which a person must decide between
RemoteHazina ya mbaliA copy of the repository on another computer, such as GitHub
PushKusukumaSending your commits to the remote
PullKuvutaBringing others’ commits from the remote
CloneKunakiliMaking a full copy of a repository, with its history
Pull requestOmbi la kuunganishaA request on GitHub to review and merge a branch

Check your understanding

  1. What is the difference between Git and GitHub?
  2. Why do results.csv and results.exe belong in .gitignore?
  3. What do git add and git commit each do, and why are they separate steps?
  4. In a git diff, what do lines beginning with - and + mean?
  5. You edited style.css, the page now looks wrong, and you have not committed. How do you get the last committed version back, and what should you check first?
  6. Why make a branch instead of trying an idea directly on main?
  7. When does Git need your help to merge, and what do the lines <<<<<<<, ======= and >>>>>>> mean?
  8. You committed a file containing the database password and pushed it to GitHub. You delete the file and push again. Are you safe? What must you do?
  9. Why must the repository be public before using GitHub Pages on a free account, and what must you check before making it public?
  10. You work on the project at home and at school. What two habits stop your two copies from getting out of step?

Answers

  1. Git is a program on your computer that keeps the history of your files. GitHub is a website that stores Git repositories online and adds tools for working together.
  2. They are made by your programs and can always be made again. Saving them in Git only adds noise and makes changes harder to read.
  3. git add chooses which changes go into the next snapshot; git commit saves that snapshot with a message. Keeping them separate lets you commit some changes and leave others for a later commit.
  4. A - line was removed; a + line was added. Unmarked lines are unchanged and show where the change is.
  5. git restore web/style.css. Check git diff first, because the uncommitted changes will be lost for good.
  6. So that main always works. If the idea fails, you delete the branch and nothing is lost; if it works, you merge it.
  7. When two branches changed the same lines in different ways. The part above ======= is your current branch’s version, the part below is the other branch’s version, and the markers show where the conflict starts and ends.
  8. No. The password is still in the history, where anyone with a copy can find it. Change the password immediately, then remove the file.
  9. Free accounts can only publish Pages from public repositories, and a public repository can be read by anyone in the world. Check that it contains no passwords, keys or real personal data — such as real students’ names and marks.
  10. Pull before you start work, and push when you stop.
Appendix

Kiswahili glossary

Every key term in the book, in alphabetical order, with the chapter that explains it.

EnglishKiswahiliMeaningChapter
AntivirusProgramu ya kuzuia virusiSoftware that finds and removes malware7
ArraySafuAn ordered list of values3
AttributeSifaExtra information inside an opening tag, such as href2
BackupNakala rudufuA copy of the database kept somewhere safe4, 7
BranchTawiA separate line of work8
BrowserKivinjariThe program you use to open websites1
ClassDarasaA bundle of data and the functions that work on it6
CloneKunakiliMaking a full copy of a repository, with its history8
Code editorKihariri msimboA program for writing code, such as VS Code2
ColourRangiWritten in CSS as a hex code such as #1b24352
ColumnSafu wimaOne kind of information in a table, such as name4
CommitKuhifadhi toleoOne saved snapshot of the project, with a message8
CompilerKikusanyajiA program that checks source code and turns it into a program the computer runs6
Computer virusVirusi vya kompyutaMalware that spreads by attaching itself to files7
ConditionShartiA question that is either true or false3
ConflictMgonganoTwo changes to the same lines, which a person must decide between8
ConstraintKizuiziA rule the data must obey, such as CHECK4
ConstructorKijenziThe function that sets up a new object6
CSV fileFaili la CSVA spreadsheet saved as plain text, with commas between cells5
DatabaseHifadhidataAn organised store of data that checks it and answers questions about it4
DebuggingKutatua hitilafuFinding and fixing mistakes in code3
DictionaryKamusiValues kept under names, called keys5
DivisionDarajaThe overall Form 4 result, worked out from points5
DownloadPakuaCopy a file from the internet to your device1
ElementKipengeleOne part of an HTML page, such as a heading or a table2
EncapsulationUfichajiKeeping data private and changing it only through checked functions6
Environment variableKigezo cha mazingiraA setting given to a program by the terminal, not written in the code5
ErrorHitilafuA problem that stops code from running as intended3
EventTukioSomething that happens on the page, such as a click3
ExceptionHitilafuAn error raised while a program runs, which can be caught5, 6
FileFailiOne saved piece of work1
FolderKabrasha (folda)A container for files and other folders1
Foreign keyUfunguo wa njeA column that holds the key of a row in another table4
FormFomuThe part of a page where visitors type or choose information2
FunctionKitendakaziA named set of steps that can be used again3
HackerMdukuziA person who breaks into computers or accounts7
HardwareMaunziThe physical parts of a computer1
Header fileFaili kichwaA .h file that declares what other files can use6
HeadingKichwaA title for a page or a section2
ImagePichaA picture shown on the page2
InheritanceUrithiA class taking on everything another class has, and adding to it6
KeyboardKibodiThe keys you type with1
LayoutMpangilioHow the parts of a page are arranged2
LibraryMaktabaReady-made code you install and use, such as psycopg5
LinkKiungoText or an image that opens another page when clicked2, 7
LinkerKiunganishiThe last step of compiling, joining all the pieces into one program6
ListOrodhaValues kept in order5
LoopKitanziCode that repeats for each item or until something is true3, 5
MalwareProgramu hasidiSoftware written to cause harm7
Memory (RAM)KumbukumbuFast, temporary space for the work you are doing now1
MergeKuunganishaBringing one branch’s work into another8
ModuleModuliA Python file whose functions other files can import5
ObjectKituA group of named values3, 6
Operating systemMfumo wa uendeshajiThe software that runs the computer and all other programs1
PasswordNenosiriA secret word used to prove who you are7
PointsPointiThe value of a grade: A is 1, F is 55
PolymorphismUpolimofuOne call running different code depending on the real object6
Primary keyUfunguo mkuuA column whose value is different for every row4
Programming languageLugha ya programuA language for giving a computer step-by-step instructions3
PullKuvutaBringing others’ commits from the remote8
Pull requestOmbi la kuunganishaA request on GitHub to review and merge a branch8
PushKusukumaSending your commits to the remote8
QueryHojaA question asked of the database in SQL4
RelationshipUhusianoA connection between two tables through keys4
RemoteHazina ya mbaliA copy of the repository on another computer, such as GitHub8
RepositoryHazinaA project whose history Git keeps8
Row (record)RekodiOne item in a table, such as one student4
ScamUtapeliA trick to steal money or information7
ServerSevaA computer that stores websites and answers requests1
ShortcutNjia ya mkatoA key combination that does a job without the mouse1
SoftwareProgramuInstructions that tell the hardware what to do1
Source codeMsimbo chanzoThe code you write, before it is compiled6
StorageHifadhiWhere files are kept when the power is off1
StringMfuatano wa herufiText, written between quotes3
StylesheetLaha ya mitindoA CSS file that controls how pages look2
TableJedwaliData arranged in rows and columns2, 4
TerminalKituo cha amriA window where you type commands to the computer5
TestJaribioCode that checks other code gives the right answers5
TransactionMuamalaSeveral changes that all happen, or none do4
Two-step verificationUthibitisho wa hatua mbiliA second check, such as a code, after your password7
TypeAinaThe kind of value a variable holds, such as int or std::string6
UpdateSasishoA new version that fixes problems and security holes7
UploadPakiaCopy a file from your device to the internet1
ValueThamaniA piece of data, such as a number or some text3
VariableKigezoA name that holds a value3
Version controlUdhibiti wa matoleoKeeping a history of every saved version of your files8
ViewMwonekanoA saved query that can be used like a table4
Web pageUkurasa wa wavutiOne document on a website, written in HTML2
WebsiteTovutiA collection of web pages under one domain name1
Ask AI