Contents

Week 8: Advanced Python, Git & GitHub, and the Month 2 Project

Month 2 | Main goal: The student writes "modern" Python (comprehensions, lambda, generators, decorators), uses Git & GitHub, and completes the Month 2 Project: a "Library System" stored on GitHub.

Teacher Preparation (read before the week starts)

Week Goals (by the end of the week the student will be able to...)

  1. Write list, dict and set comprehensions (including if).
  2. Use lambda together with sorted(key=...), map, filter, max/min(key=...).
  3. Explain what an iterator is (iter, next, StopIteration) and write a generator with yield.
  4. Write a simple decorator (including *args, **kwargs and functools.wraps).
  5. Use Git: init, status, add, commit, log, .gitignore.
  6. Use GitHub: create a repo, remote add, push.
  7. Build and upload to GitHub a library project that uses OOP + JSON + exceptions.

Session 1 — Comprehensions, lambda, map/filter/sorted

Session flow

  1. Review of the Week 7 homework (especially Rafu — we will need it in Session 3)
  2. List comprehensions (+ if, + if/else)
  3. Dict and set comprehensions; a generator expression inside sum()
  4. Break
  5. lambda
  6. sorted(key=...), max/min(key=...), map, filter
  7. In-class exercise
  8. Q&A + homework

Explanation for the teacher (read this first)

A list comprehension is a short way of creating a new list from an iterable:

text
[KITU for kipengele in iterable]
[KITU for kipengele in iterable if SHARTI]          # chuja
[A if SHARTI else B for kipengele in iterable]      # badilisha kwa masharti

It is exactly equivalent to a for loop + append. The reading rule: start with for, then if, and finally look at the item at the start ("for each x in the numbers, if x is even, take x*x").

A caution on order: a filtering if comes at the end; a transforming if/else comes at the start (before the for). [x for x in n if x > 0 else 0] is a SyntaxError.

  • Dict comprehension: {k: v for ... in ...} → dict.
  • Set comprehension: {x for ... in ...} → set (no duplicates).
  • Generator expression: (x for ...) — does not build a whole list; it gives items one at a time. Inside a function: sum(x * x for x in namba) (no extra parentheses).
  • Do not use very long comprehensions — if one goes past a single line and becomes hard to read, go back to an ordinary loop.

lambda is a small unnamed function with one expression (no return, no multiple lines):

python
mraba = lambda x: x * x      # sawa na def mraba(x): return x * x

We do not usually store it in a variable (PEP 8 says to use def); we use it on the spot as an argument to sorted, max, map, filter.

sorted(iterable, key=f, reverse=False) — key is a function called on each element, and the sorting is done by the returned value. E.g. sorted(maneno, key=len). Also list.sort(key=...) (changes the list itself, returns None). max(..., key=...) and min(..., key=...) work the same way.

map(f, iterable) — calls f on each element. filter(f, iterable) — keeps the elements for which f returns True. Both return an iterator (not a list) — you need list(...) to see the results. In modern Python, comprehensions often read better than map/filter — but you must know them because you will see them.

Say this to the student

  • "A comprehension is like telling an assistant in one sentence: 'For each student in the class, if they passed, bring me their name.' Instead of giving instructions step by step."
  • "lambda is like writing on a small slip of paper (sticky note) — short, one-time instructions that do not need a whole notebook (def)."
  • "key= is telling sorted 'sort by what': by length? by price? by age? It is like lining students up by their height instead of by their names."

Type this code live (share screen)

Step 1: Loop vs comprehension — comprehensions.py

python
namba = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Njia ya zamani
mraba = []
for n in namba:
    mraba.append(n * n)
print(mraba)

# Comprehension
mraba = [n * n for n in namba]
print(mraba)

shufwa = [n for n in namba if n % 2 == 0]
print(shufwa)

hali = ["shufwa" if n % 2 == 0 else "witiri" for n in namba[:4]]
print(hali)

Output:

text
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[2, 4, 6, 8, 10]
['witiri', 'shufwa', 'witiri', 'shufwa']

Step 2: Real examples

python
majina = ["  asha ", "JUMA", "neema  ", ""]
safi = [j.strip().title() for j in majina if j.strip()]
print(safi)                                  # ['Asha', 'Juma', 'Neema']

bei_usd = [10, 25, 40]
bei_tzs = [b * 2600 for b in bei_usd]
print(bei_tzs)                               # [26000, 65000, 104000]

maandishi = "python ni rahisi na python ni nzuri"
herufi_za_kwanza = {neno[0] for neno in maandishi.split()}
print(sorted(herufi_za_kwanza))              # ['n', 'p', 'r']

urefu = {neno: len(neno) for neno in ["embe", "nanasi", "papai"]}
print(urefu)                                 # {'embe': 4, 'nanasi': 6, 'papai': 5}

alama = {"Asha": 88, "Juma": 42, "Neema": 67}
waliofaulu = {jina: a for jina, a in alama.items() if a >= 50}
print(waliofaulu)                            # {'Asha': 88, 'Neema': 67}

print(sum(a for a in alama.values()))        # 197  (generator expression)

jedwali = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(jedwali)                               # [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

Step 3: lambda + sorted — lambda_demo.py

python
def mraba(x):
    return x * x

mraba2 = lambda x: x * x
print(mraba(5), mraba2(5))          # 25 25

maneno = ["nanasi", "embe", "papai", "ndizi"]
print(sorted(maneno))                       # kwa alfabeti
print(sorted(maneno, key=len))              # kwa urefu
print(sorted(maneno, key=len, reverse=True))

wanafunzi = [("Asha", 88), ("Juma", 42), ("Neema", 67)]
print(sorted(wanafunzi, key=lambda w: w[1], reverse=True))
print(max(wanafunzi, key=lambda w: w[1]))

bidhaa = [
    {"jina": "Sukari", "bei": 3000},
    {"jina": "Chumvi", "bei": 800},
    {"jina": "Mafuta", "bei": 7500},
]
nafuu_kwanza = sorted(bidhaa, key=lambda b: b["bei"])
print([b["jina"] for b in nafuu_kwanza])

Output:

text
25 25
['embe', 'nanasi', 'ndizi', 'papai']
['embe', 'papai', 'ndizi', 'nanasi']
['nanasi', 'papai', 'ndizi', 'embe']
[('Asha', 88), ('Neema', 67), ('Juma', 42)]
('Asha', 88)
['Chumvi', 'Sukari', 'Mafuta']

Explain: sorted is "stable" — words of equal length (papai, ndizi) stay in their original order.

Step 4: map and filter

python
namba = [1, 2, 3, 4, 5]
print(map(lambda x: x * 10, namba))              # <map object at 0x...>
print(list(map(lambda x: x * 10, namba)))        # [10, 20, 30, 40, 50]
print(list(filter(lambda x: x % 2 == 1, namba))) # [1, 3, 5]

maandishi = ["12", "7", "30"]
print(list(map(int, maandishi)))                 # [12, 7, 30]
print(sum(map(int, input("Andika namba kwa nafasi: ").split())))

Say: "map(int, ...) is a very quick way of converting strings into numbers — remember input().split()."

Step 5: Objects (Week 7) + key

python
from dataclasses import dataclass

@dataclass
class Mwanafunzi:
    jina: str
    alama: int

darasa = [Mwanafunzi("Asha", 88), Mwanafunzi("Juma", 42), Mwanafunzi("Neema", 67)]
for m in sorted(darasa, key=lambda m: m.alama, reverse=True):
    print(m.jina, m.alama)
print("Wastani:", sum(m.alama for m in darasa) / len(darasa))

Questions to ask the student

  1. What does [x * 2 for x in range(5)] give? — [0, 2, 4, 6, 8].
  2. Where does a filtering if go in a comprehension? — At the end, after the for.
  3. What does {x for x in [1, 1, 2]} give? — The set {1, 2}.
  4. Can a lambda have multiple lines? — No, one expression only.
  5. What does key=len mean in sorted? — Sort by the length of each element.
  6. Why does print(map(...)) show <map object>? — map returns an iterator; use list().
  7. What is the difference between sorted(lst) and lst.sort()? — sorted returns a new list; .sort() changes the same list and returns None.

Common mistakes

Mistake Error message Fix
if/else at the end: [x for x in n if x > 0 else 0] SyntaxError: invalid syntax (or expected 'else' after 'if' expression) [x if x > 0 else 0 for x in n]
A lambda with return SyntaxError: invalid syntax lambda x: x * 2 (no return)
x = lst.sort() and then using x TypeError: 'NoneType' object is not iterable x = sorted(lst)
key=len() (calling the function) TypeError: len() takes exactly one argument (0 given) key=len without ()
Using a map object twice The second time it is empty [] An iterator runs out; store it as list(...).
Sorting a list with str and int together TypeError: '<' not supported between instances of 'int' and 'str' Convert all to one type, or key=str.

In-class exercise

Exercise 1.1: From joto_c = [22, 25, 31, 18, 29] create a list of Fahrenheit values with a comprehension, and a list of the days hotter than 26°C.

Exercise 1.2: From a sentence, create a dict of neno: idadi_ya_herufi (word: number of letters) only for words with more than 3 letters.

Exercise 1.3: bidhaa = [("Sukari", 3000, 20), ("Mchele", 2500, 6), ("Sabuni", 1200, 3), ("Mafuta", 7500, 12)] (name, price, quantity). (a) Sort by stock value (price×quantity) from largest. (b) Find the most expensive product with max(key=...). (c) The names of products with quantity < 10 using filter and then using a comprehension.

Answer
python
# 1.1
joto_c = [22, 25, 31, 18, 29]
joto_f = [c * 9 / 5 + 32 for c in joto_c]
print(joto_f)                       # [71.6, 77.0, 87.8, 64.4, 84.2]
print([c for c in joto_c if c > 26])   # [31, 29]

# 1.2
sentensi = "Leo ni siku nzuri ya kujifunza Python"
print({n: len(n) for n in sentensi.split() if len(n) > 3})
# {'siku': 4, 'nzuri': 5, 'kujifunza': 9, 'Python': 6}

# 1.3
bidhaa = [("Sukari", 3000, 20), ("Mchele", 2500, 6), ("Sabuni", 1200, 3), ("Mafuta", 7500, 12)]
print(sorted(bidhaa, key=lambda b: b[1] * b[2], reverse=True))
# [('Mafuta', 7500, 12), ('Sukari', 3000, 20), ('Mchele', 2500, 6), ('Sabuni', 1200, 3)]
print(max(bidhaa, key=lambda b: b[1]))                 # ('Mafuta', 7500, 12)
print([b[0] for b in filter(lambda b: b[2] < 10, bidhaa)])   # ['Mchele', 'Sabuni']
print([b[0] for b in bidhaa if b[2] < 10])                   # ['Mchele', 'Sabuni']

Session 2 — Iterators, Generators, Decorators, *args/**kwargs and Git & GitHub

Session flow

  1. Short review of the homework
  2. Iterators: iter, next, StopIteration
  3. Generators: yield
  4. Review of *args, **kwargs
  5. Decorators
  6. Break
  7. Git: config, init, status, add, commit, log, .gitignore
  8. GitHub: create a repo, remote add, push
  9. Q&A + homework (project preparation)

This session has a lot in it — if time is tight, cut decorators down to one example (@muda), but DO NOT SKIP Git: we need it in Session 3.

Explanation for the teacher (read this first)

Iterable vs iterator. An iterable is something you can loop over with for (list, str, dict, file). An iterator is a "finger" that remembers where you are: it = iter(lst), then next(it) gives the next item; when they run out → StopIteration. A for loop does this behind the scenes. Once an iterator has run out it does not start again (which is why the map in Session 1 was empty the second time).

A generator is a function with yield instead of (or along with) return. When you call it, it does not run the code immediately — it returns a generator object (a kind of iterator). Each next() runs the code up to the next yield, gives a value, and pauses, remembering its state. The benefit: it uses little memory — it gives items one at a time instead of building a whole list (e.g. reading a 2 GB file line by line, or an endless series).

*args and **kwargs (review of Week 4): *args collects extra positional arguments into a tuple; **kwargs collects keyword arguments into a dict. The names args/kwargs are a convention; * and ** are what matter. They are also used for unpacking: f(*lst), f(**d).

A decorator is a function that receives another function and returns a new "wrapped" function (wrapper) with extra behaviour — without changing the original function's code. Steps to understanding it:

  1. Functions are objects — they can be passed as arguments (we have done this with key=!).
  2. A function can be defined inside another function and returned.
  3. @decorator above def f is shorthand for f = decorator(f).

The wrapper uses *args, **kwargs so that it works with any function, and @functools.wraps(func) so that the original function's name and docstring are not lost. We have already seen decorators: @property, @dataclass, @abstractmethod.

Git is a "version control" program — it saves the history of changes to your code as "snapshots" (commits). You can go back, see what changed, and work with others. GitHub is a website that stores Git repositories online (backup + sharing + a portfolio of work). Git ≠ GitHub: Git is on your computer; GitHub is an online "server".

Git's three areas:

text
Working directory  --git add-->  Staging area  --git commit-->  Repository (historia)
 (unahariri files)              (umechagua cha kuhifadhi)       (picha imehifadhiwa)
                                                    --git push--> GitHub

Commands (PowerShell, inside the project folder):

powershell
git config --global user.name "Jina Lako"            # mara moja tu kwa kompyuta
git config --global user.email "email@ya-github.com"
git config --global init.defaultBranch main
git init                    # anzisha repo (folder lililofichwa .git)
git status                  # nini kimebadilika?
git add jina.py             # au: git add .   (kila kitu)
git commit -m "Ujumbe unaoeleza mabadiliko"
git log --oneline           # historia

.gitignore is a file listing the files/folders Git should not track: .venv/, __pycache__/, *.pyc, .vscode/, and personal data. It should be created before the first git add ..

Uploading to GitHub: on github.com → "+" → New repository → name (e.g. maktaba) → Public → DO NOT TICK "Add a README" (the repo should be empty, to avoid conflicts) → Create. Then:

powershell
git remote add origin https://github.com/JINA_LAKO/maktaba.git
git branch -M main
git push -u origin main

The first time, Windows will open a Git Credential Manager window — choose "Sign in with your browser" and allow it. After that, git push on its own is enough. (GitHub has not accepted account passwords in the terminal since 2021 — browser login or a Personal Access Token only.)

Say this to the student

  • "An iterator is like a book with a bookmark: you read one page, put in the marker, and tomorrow you carry on from the same place. When you finish the book, you are done."
  • "A generator is like a chapati cook who hands you them one by one as they are ready, instead of waiting to cook 100 before bringing them all. They rest (yield) after each one until you ask for another (next)."
  • "A decorator is like wrapping a gift in coloured paper: the gift (function) does not change inside, but now it has something extra on the outside."
  • "Git is like the 'Save Game' button in a video game. Each commit is a place you can return to if you make a mistake. GitHub is storing your 'saves' in the cloud so that even if your computer is stolen, your work is safe."
  • "git add is putting things in a box; git commit is sealing the box and writing a label; git push is posting it."

Type this code live (share screen)

Step 1: Iterator — iterators.py

python
matunda = ["embe", "nanasi", "papai"]
it = iter(matunda)
print(next(it))      # embe
print(next(it))      # nanasi
print(next(it))      # papai
try:
    print(next(it))
except StopIteration:
    print("Vimeisha!")

Step 2: Generator

python
def hesabu_hadi(n):
    print("Naanza...")
    i = 1
    while i <= n:
        yield i
        i += 1
    print("Nimemaliza.")

g = hesabu_hadi(3)
print(g)                 # <generator object hesabu_hadi at 0x...>
print(next(g))           # Naanza...  1
print(next(g))           # 2
for x in g:              # inaendelea pale ilipoachwa
    print("loop:", x)

Output:

text
<generator object hesabu_hadi at 0x...>
Naanza...
1
2
loop: 3
Nimemaliza.

Emphasise: "Naanza..." (starting...) was not printed when we called hesabu_hadi(3) — it waited for the first next.

A useful example:

python
import sys

def namba_shufwa(kikomo):
    for n in range(kikomo):
        if n % 2 == 0:
            yield n

list_kubwa = [n for n in range(1_000_000) if n % 2 == 0]
gen = namba_shufwa(1_000_000)
print(sys.getsizeof(list_kubwa), "bytes")   # takriban 4,000,000+
print(sys.getsizeof(gen), "bytes")          # takriban 200
print(sum(namba_shufwa(1_000_000)))         # 249999500000


def soma_mistari(path):
    with open(path, encoding="utf-8") as f:
        for mstari in f:
            mstari = mstari.strip()
            if mstari and not mstari.startswith("#"):
                yield mstari

(The actual getsizeof numbers vary slightly between versions — what matters is the big difference.)

An endless series:

python
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])   # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Step 3: *args, **kwargs — args_kwargs.py

python
def jumla(*args):
    print("args:", args)
    return sum(args)

print(jumla(1, 2, 3))

def wasifu(jina, **kwargs):
    print("kwargs:", kwargs)
    for k, v in kwargs.items():
        print(f"  {k}: {v}")

wasifu("Asha", umri=17, mji="Arusha")

namba = [4, 5, 6]
print(jumla(*namba))                    # kufungua list
taarifa = {"umri": 20, "kazi": "Mwalimu"}
wasifu("Juma", **taarifa)               # kufungua dict

Output:

text
args: (1, 2, 3)
6
kwargs: {'umri': 17, 'mji': 'Arusha'}
  umri: 17
  mji: Arusha
args: (4, 5, 6)
15
kwargs: {'umri': 20, 'kazi': 'Mwalimu'}
  umri: 20
  kazi: Mwalimu

Step 4: Decorators — decorators.py. Build it up step by step:

python
# (a) functions ni objects
def salimia(jina):
    return f"Habari {jina}"

f = salimia
print(f("Asha"))

# (b) function inayorudisha function
def tengeneza_kizidishi(n):
    def zidisha(x):
        return x * n
    return zidisha

mara3 = tengeneza_kizidishi(3)
print(mara3(10))          # 30
python
# (c) decorator kwa mkono
def kelele(func):
    def wrapper(*args, **kwargs):
        jibu = func(*args, **kwargs)
        return jibu.upper() + "!!!"
    return wrapper

def salimia(jina):
    return f"Habari {jina}"

salimia = kelele(salimia)       # kufunika kwa mkono
print(salimia("Asha"))          # HABARI ASHA!!!


# (d) syntax ya @ -- sawa kabisa na hapo juu
@kelele
def aga(jina):
    return f"Kwaheri {jina}"

print(aga("Juma"))              # KWAHERI JUMA!!!

Now some useful decorators:

python
import functools
import time


def muda(func):
    """Inapima muda wa function kuendeshwa."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        mwanzo = time.perf_counter()
        jibu = func(*args, **kwargs)
        mwisho = time.perf_counter()
        print(f"[{func.__name__}] ilichukua sekunde {mwisho - mwanzo:.4f}")
        return jibu
    return wrapper


def rekodi(func):
    """Inaandika function ilipoitwa na arguments zake."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"-> {func.__name__} args={args} kwargs={kwargs}")
        jibu = func(*args, **kwargs)
        print(f"<- {func.__name__} imerudisha {jibu!r}")
        return jibu
    return wrapper


@muda
def hesabu_kubwa(n):
    return sum(i * i for i in range(n))


@rekodi
def ongeza(a, b=0):
    return a + b


print(hesabu_kubwa(1_000_000))
ongeza(2, b=3)
print(ongeza.__name__)       # ongeza  (shukrani kwa functools.wraps)

Output (the timing will differ):

text
[hesabu_kubwa] ilichukua sekunde 0.0612
333332833333500000
-> ongeza args=(2,) kwargs={'b': 3}
<- ongeza imerudisha 5
ongeza

Show: remove @functools.wraps(func) → ongeza.__name__ becomes wrapper. "That is why we add wraps."

Step 5: Git — in the VS Code terminal, inside wiki08:

powershell
git --version
git config --global user.name "Mwalimu Selanim"
git config --global user.email "email-yako@example.com"
git config --global init.defaultBranch main
git config --list

(Tell the student to use the name and email of their GitHub account.)

Create a test folder:

powershell
mkdir git_majaribio
cd git_majaribio
git init
git status

Output: Initialized empty Git repository in C:/.../git_majaribio/.git/ and then On branch main ... nothing to commit.

Create salamu.py (print("Habari Git!")), then:

powershell
git status                 # salamu.py ni "Untracked" (nyekundu)
git add salamu.py
git status                 # "Changes to be committed" (kijani)
git commit -m "Ongeza programu ya salamu"
git log --oneline

Change salamu.py (add a line), then:

powershell
git status                 # modified
git diff                   # inaonyesha mistari iliyobadilika (+ kijani, - nyekundu)
git add .
git commit -m "Ongeza salamu ya pili"
git log --oneline

Sample output:

text
b4e1c2a (HEAD -> main) Ongeza salamu ya pili
7a9d3f0 Ongeza programu ya salamu

.gitignore — create a venv and show the problem:

powershell
python -m venv .venv
git status                 # .venv/ inaonekana -- hatutaki!

Create the file .gitignore (in VS Code: New File → .gitignore):

text
.venv/
__pycache__/
*.pyc
.vscode/
powershell
git status                 # .venv imetoweka; .gitignore yenyewe inaonekana
git add .gitignore
git commit -m "Ongeza .gitignore"

Also show the Source Control button (Ctrl+Shift+G) in VS Code — it does add/commit by clicking. But we learn the commands first.

Step 6: GitHub — share the browser: github.com → "+" → New repository → git_majaribio → Public → no README → Create. Copy the commands from "…or push an existing repository from the command line":

powershell
git remote add origin https://github.com/JINA_LAKO/git_majaribio.git
git branch -M main
git push -u origin main

Log in through the browser if asked. Refresh the GitHub page — the files have arrived! Change something, add, commit, then git push on its own.

Questions to ask the student

  1. What happens if you call next() on an exhausted iterator? — StopIteration.
  2. What is the difference between return and yield? — return ends the function; yield gives a value and pauses, carrying on at the next next.
  3. Why does a generator use little memory? — It does not build a whole list; it gives items one at a time.
  4. What type is *args inside a function? **kwargs? — Tuple; dict.
  5. @kelele above def aga is the same as what? — aga = kelele(aga).
  6. Why should a wrapper use *args, **kwargs? — So that it works with any function, with any arguments.
  7. What are the three steps for saving changes to GitHub? — git add, git commit -m "...", git push.
  8. Why should .venv/ be in .gitignore? — It is large and can be recreated from requirements.txt.

Common mistakes

Mistake Error message Fix
Git is not installed / old terminal git : The term 'git' is not recognized as the name of a cmdlet... Install Git, close and reopen VS Code.
You did not set a name/email Author identity unknown *** Please tell me who you are. git config --global user.name ... and user.email ...
Commit without -m It opens an editor (Vim/VS Code) — the student gets stuck In Vim: type :q! Enter; then use -m "ujumbe".
git commit without add nothing added to commit but untracked files present git add . first
git init in the wrong folder (e.g. all of Documents) Thousands of "untracked" files Delete the hidden .git folder (Remove-Item -Recurse -Force .git) and cd to the right folder.
The GitHub repo has a README, push is rejected ! [rejected] main -> main (fetch first) git pull origin main --allow-unrelated-histories then git push, or create the repo without a README.
remote origin already exists error: remote origin already exists. git remote set-url origin <URL sahihi> (the correct URL)
Branch name error error: src refspec main does not match any There is no commit yet, or the branch is master: make a commit first / git branch -M main
Calling a generator and expecting a list print(gen) shows <generator object ...> list(gen) or for
The decorator forgets return jibu The decorated function returns None return func(*args, **kwargs)

In-class exercise

Exercise 2.1: A generator hesabu_kurudi_nyuma(n) (countdown) that gives n, n-1, ..., 1 and then "Uzinduzi!" (Launch!).

Exercise 2.2: A generator vipande(lst, ukubwa) (chunks; size) that splits a list into pieces: list(vipande([1,2,3,4,5], 2)) → [[1, 2], [3, 4], [5]].

Exercise 2.3: A decorator jaribu_tena(func) (try again) — if the function raises an exception, it tries again up to 3 times before giving up (raise the last error).

Exercise 2.4 (Git): The student pushes their Week 7 work (folder wiki07) to a new GitHub repo python-kozi, with a .gitignore and at least 2 commits.

Answer
python
# 2.1
def hesabu_kurudi_nyuma(n):
    while n > 0:
        yield n
        n -= 1
    yield "Uzinduzi!"

for x in hesabu_kurudi_nyuma(3):
    print(x)      # 3 2 1 Uzinduzi!

# 2.2
def vipande(lst, ukubwa):
    for i in range(0, len(lst), ukubwa):
        yield lst[i:i + ukubwa]

print(list(vipande([1, 2, 3, 4, 5], 2)))   # [[1, 2], [3, 4], [5]]

# 2.3
import functools
import random

def jaribu_tena(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        kosa_la_mwisho = None
        for jaribio in range(1, 4):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                print(f"Jaribio {jaribio} limeshindwa: {e}")
                kosa_la_mwisho = e
        raise kosa_la_mwisho
    return wrapper

@jaribu_tena
def mtandao_dhaifu():
    if random.random() < 0.6:
        raise ConnectionError("Mtandao umekatika")
    return "Data imepakuliwa"

try:
    print(mtandao_dhaifu())
except ConnectionError:
    print("Imeshindikana baada ya majaribio 3.")

2.4 — commands:

powershell
cd ..\wiki07
git init
# tengeneza .gitignore (.venv/, __pycache__/, *.pyc, .vscode/)
git add .gitignore
git commit -m "Ongeza .gitignore"
git add .
git commit -m "Kazi za Wiki 7: inheritance na dataclasses"
git remote add origin https://github.com/JINA_LAKO/python-kozi.git
git branch -M main
git push -u origin main

Session 3 — Month 2 Project: "Library System"

Session flow

  1. Review: did Git work? Is the Rafu from Week 7 ready?
  2. Project explanation, file structure, git init + .gitignore
  3. models.py — dataclasses Kitabu, Mwanachama + exceptions (makosa.py)
  4. maktaba.py — class Maktaba (logic) + saving/loading JSON
  5. Break (commit first!)
  6. main.py — terminal menu, handling errors
  7. README, final commit, git push to GitHub
  8. Month 2 assessment, holiday instructions.

Explanation for the teacher (read this first)

This project combines everything from Month 2: | Week | What is used in the project | |------|-----------------------------| | 5 | JSON, pathlib, try/except, custom exceptions | | 6 | Classes, methods, encapsulation (_vitabu), __str__ | | 7 | @dataclass, exception inheritance, type hints, __len__ | | 8 | Comprehensions, lambda + sorted, generator, decorator, Git/GitHub |

Project structure:

text
maktaba/
├── .gitignore
├── README.md
├── makosa.py      # custom exceptions
├── models.py      # Kitabu, Mwanachama (dataclasses)
├── maktaba.py     # class Maktaba: logic + JSON
├── main.py        # menu ya mtumiaji
└── data.json      # inaundwa yenyewe (iko kwenye .gitignore? -- tazama chini)

Decision about data.json: we put it in .gitignore because it is user data, not code. (Tell the student: in a real project, people's data does not go on GitHub.)

How to teach it: You write makosa.py and models.py live while explaining. Write maktaba.py together — the student writes the rudisha (return) and tafuta (search) methods themselves with your guidance. main.py — give them the structure, and they write part of the menu. Commit after every file! If time runs out, the student can finish during the holiday (it is in the optional work).

Small new things in the project:

  • asdict(obj) (from dataclasses) → a dict for JSON; Kitabu(**d) → turns a dict back into an object (this is where the **kwargs from Session 2 proves useful!).
  • @classmethod — a method that receives the class (cls) instead of an object; it is used as an "alternative constructor": Maktaba.pakia(path). Explain: "It is another way of creating an object — here, from a file."
  • date.today().isoformat() → "2026-12-24" (a string, suitable for JSON). date.fromisoformat(s) to convert back.

Say this to the student

  • "Today you are the developer of the school library. The librarian wants the computer to remember all the books, the members, who has borrowed what, and how many days late a book is."
  • "We divide the code into files like departments in a company: models is the records department, maktaba is the manager who makes decisions, main is the reception that talks to customers."
  • "Commit at every step — like saving the game before a hard level."

Type this code live (share screen)

Step 1: Start the project

powershell
mkdir maktaba
cd maktaba
git init
code .gitignore

.gitignore:

text
.venv/
__pycache__/
*.pyc
.vscode/
data.json

(This project does not use external packages — requirements.txt is not required. If the student wants, python -m venv .venv is still a good habit.)

Step 2: makosa.py

python
"""Makosa (exceptions) maalum ya mfumo wa maktaba."""


class KosaLaMaktaba(Exception):
    """Mzazi wa makosa yote ya maktaba."""


class KitabuHakipo(KosaLaMaktaba):
    pass


class MwanachamaHayupo(KosaLaMaktaba):
    pass


class KimeshaAzimwa(KosaLaMaktaba):
    pass


class HakijaAzimwa(KosaLaMaktaba):
    pass


class KikomoKimefikiwa(KosaLaMaktaba):
    pass


class TayariKipo(KosaLaMaktaba):
    pass

Explain: a docstring inside a class is enough — we do not need pass if there is a docstring.

Step 3: models.py

python
"""Data za mfumo: Kitabu na Mwanachama."""
from dataclasses import dataclass, field


@dataclass
class Kitabu:
    isbn: str
    kichwa: str
    mwandishi: str
    mwaka: int
    aliyeazima: str | None = None          # namba ya mwanachama
    tarehe_ya_kuazima: str | None = None   # "YYYY-MM-DD"

    @property
    def kimeazimwa(self) -> bool:
        return self.aliyeazima is not None

    def __str__(self) -> str:
        hali = f"kimeazimwa na {self.aliyeazima}" if self.kimeazimwa else "kipo"
        return f"[{self.isbn}] {self.kichwa} - {self.mwandishi} ({self.mwaka}) | {hali}"


@dataclass
class Mwanachama:
    namba: str
    jina: str
    vitabu: list[str] = field(default_factory=list)    # ISBN za vitabu alivyoazima

    def __str__(self) -> str:
        return f"{self.namba}: {self.jina} (ameazima {len(self.vitabu)})"

Test quickly in the terminal:

powershell
python -c "from models import Kitabu; k = Kitabu('1', 'Kinjeketile', 'E. Hussein', 1969); print(k)"

Output: [1] Kinjeketile - E. Hussein (1969) | kipo

powershell
git add .
git commit -m "Ongeza models na makosa"

Step 4: maktaba.py (the heart of the project)

python
"""Logic ya maktaba na kuhifadhi kwenye JSON."""
import functools
import json
from dataclasses import asdict
from datetime import date
from pathlib import Path
from typing import Iterator

from makosa import (
    HakijaAzimwa,
    KikomoKimefikiwa,
    KimeshaAzimwa,
    KitabuHakipo,
    MwanachamaHayupo,
    TayariKipo,
)
from models import Kitabu, Mwanachama

SIKU_ZA_KUAZIMA = 14
FAINI_KWA_SIKU = 200       # TSh


def hifadhi_baadaye(method):
    """Decorator: baada ya method kufanikiwa, hifadhi maktaba kwenye file."""
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        jibu = method(self, *args, **kwargs)
        self.hifadhi()
        return jibu
    return wrapper


class Maktaba:
    KIKOMO_CHA_VITABU = 3

    def __init__(self, path: Path):
        self.path = Path(path)
        self._vitabu: dict[str, Kitabu] = {}
        self._wanachama: dict[str, Mwanachama] = {}

    # ---------- kuhifadhi / kupakia ----------
    def hifadhi(self) -> None:
        data = {
            "vitabu": [asdict(k) for k in self._vitabu.values()],
            "wanachama": [asdict(m) for m in self._wanachama.values()],
        }
        with open(self.path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

    @classmethod
    def pakia(cls, path: Path) -> "Maktaba":
        maktaba = cls(path)
        try:
            with open(maktaba.path, encoding="utf-8") as f:
                data = json.load(f)
        except FileNotFoundError:
            return maktaba                     # maktaba mpya, tupu
        except json.JSONDecodeError:
            print("Onyo: data.json imeharibika, naanza na maktaba tupu.")
            return maktaba
        for d in data.get("vitabu", []):
            k = Kitabu(**d)
            maktaba._vitabu[k.isbn] = k
        for d in data.get("wanachama", []):
            m = Mwanachama(**d)
            maktaba._wanachama[m.namba] = m
        return maktaba

    # ---------- kutafuta ----------
    def kitabu(self, isbn: str) -> Kitabu:
        try:
            return self._vitabu[isbn]
        except KeyError:
            raise KitabuHakipo(f"Hakuna kitabu chenye ISBN '{isbn}'.") from None

    def mwanachama(self, namba: str) -> Mwanachama:
        try:
            return self._wanachama[namba]
        except KeyError:
            raise MwanachamaHayupo(f"Hakuna mwanachama namba '{namba}'.") from None

    def tafuta(self, neno: str) -> list[Kitabu]:
        neno = neno.lower()
        return [
            k for k in self._vitabu.values()
            if neno in k.kichwa.lower() or neno in k.mwandishi.lower()
        ]

    def vitabu_vilivyopangwa(self, kwa: str = "kichwa") -> list[Kitabu]:
        return sorted(self._vitabu.values(), key=lambda k: getattr(k, kwa))

    def vinavyopatikana(self) -> list[Kitabu]:
        return [k for k in self._vitabu.values() if not k.kimeazimwa]

    def vilivyochelewa(self, leo: date | None = None) -> Iterator[tuple[Kitabu, int]]:
        """Generator: inatoa (kitabu, siku_za_kuchelewa) kwa vitabu vilivyopitiliza muda."""
        leo = leo or date.today()
        for k in self._vitabu.values():
            if k.tarehe_ya_kuazima:
                siku = (leo - date.fromisoformat(k.tarehe_ya_kuazima)).days
                if siku > SIKU_ZA_KUAZIMA:
                    yield k, siku - SIKU_ZA_KUAZIMA

    # ---------- kubadilisha ----------
    @hifadhi_baadaye
    def ongeza_kitabu(self, kitabu: Kitabu) -> None:
        if kitabu.isbn in self._vitabu:
            raise TayariKipo(f"ISBN '{kitabu.isbn}' tayari ipo.")
        self._vitabu[kitabu.isbn] = kitabu

    @hifadhi_baadaye
    def sajili(self, mwanachama: Mwanachama) -> None:
        if mwanachama.namba in self._wanachama:
            raise TayariKipo(f"Mwanachama '{mwanachama.namba}' tayari yupo.")
        self._wanachama[mwanachama.namba] = mwanachama

    @hifadhi_baadaye
    def azima(self, isbn: str, namba: str, leo: date | None = None) -> None:
        k = self.kitabu(isbn)
        m = self.mwanachama(namba)
        if k.kimeazimwa:
            raise KimeshaAzimwa(f"'{k.kichwa}' kimeshaazimwa na {k.aliyeazima}.")
        if len(m.vitabu) >= self.KIKOMO_CHA_VITABU:
            raise KikomoKimefikiwa(
                f"{m.jina} tayari ana vitabu {self.KIKOMO_CHA_VITABU}."
            )
        k.aliyeazima = m.namba
        k.tarehe_ya_kuazima = (leo or date.today()).isoformat()
        m.vitabu.append(k.isbn)

    @hifadhi_baadaye
    def rudisha(self, isbn: str, leo: date | None = None) -> int:
        """Inarudisha faini (TSh) kama kitabu kimechelewa, vinginevyo 0."""
        k = self.kitabu(isbn)
        if not k.kimeazimwa:
            raise HakijaAzimwa(f"'{k.kichwa}' hakijaazimwa.")
        leo = leo or date.today()
        siku = (leo - date.fromisoformat(k.tarehe_ya_kuazima)).days
        faini = max(0, siku - SIKU_ZA_KUAZIMA) * FAINI_KWA_SIKU
        m = self.mwanachama(k.aliyeazima)
        m.vitabu.remove(k.isbn)
        k.aliyeazima = None
        k.tarehe_ya_kuazima = None
        return faini

    # ---------- dunders ----------
    def __len__(self) -> int:
        return len(self._vitabu)

    def __contains__(self, isbn: str) -> bool:
        return isbn in self._vitabu

    def __str__(self) -> str:
        return (f"Maktaba: vitabu {len(self)}, vinavyopatikana "
                f"{len(self.vinavyopatikana())}, wanachama {len(self._wanachama)}")

Points to say as you type:

  • Kitabu(**d) — the dict {"isbn": "1", "kichwa": ...} is unpacked into keyword arguments. This is why the JSON keys must match the field names.
  • @hifadhi_baadaye — our own decorator! Every change is saved automatically; if we forget to call hifadhi() there is no problem. If a method raises an exception, self.hifadhi() is not reached — bad data is not saved.
  • vilivyochelewa (overdue items) is a generator (yield).
  • The leo (today) parameter lets us "pretend" it is another date in order to test fines (testing).

Quick test — jaribio.py (not part of the project, you can delete it later):

python
from datetime import date
from pathlib import Path

from maktaba import Maktaba
from makosa import KosaLaMaktaba
from models import Kitabu, Mwanachama

path = Path(__file__).parent / "jaribio.json"
path.unlink(missing_ok=True)            # anza upya kila mara

m = Maktaba.pakia(path)
m.ongeza_kitabu(Kitabu("978-1", "Kinjeketile", "Ebrahim Hussein", 1969))
m.ongeza_kitabu(Kitabu("978-2", "Rosa Mistika", "Euphrase Kezilahabi", 1971))
m.ongeza_kitabu(Kitabu("978-3", "Siku ya Watenzi Wote", "Shaaban Robert", 1968))
m.sajili(Mwanachama("M001", "Asha"))

m.azima("978-1", "M001", leo=date(2026, 12, 1))
print(m)
try:
    m.azima("978-1", "M001")
except KosaLaMaktaba as e:
    print("Kosa:", e)

for k, siku in m.vilivyochelewa(leo=date(2026, 12, 24)):
    print(f"Kimechelewa: {k.kichwa} siku {siku}")

faini = m.rudisha("978-1", leo=date(2026, 12, 24))
print("Faini:", faini)

m2 = Maktaba.pakia(path)                # pakia upya kutoka JSON
print(m2, "| '978-2' in m2:", "978-2" in m2)
print([k.kichwa for k in m2.vitabu_vilivyopangwa("mwaka")])
print([str(k) for k in m2.tafuta("robert")])

Output:

text
Maktaba: vitabu 3, vinavyopatikana 2, wanachama 1
Kosa: 'Kinjeketile' kimeshaazimwa na M001.
Kimechelewa: Kinjeketile siku 9
Faini: 1800
Maktaba: vitabu 3, vinavyopatikana 3, wanachama 1 | '978-2' in m2: True
['Siku ya Watenzi Wote', 'Kinjeketile', 'Rosa Mistika']
['[978-3] Siku ya Watenzi Wote - Shaaban Robert (1968) | kipo']

Fine calculation: Dec 1 → Dec 24 = 23 days; 23 − 14 = 9 × 200 = 1,800. Open jaribio.json and show the data. Add jaribio.json to .gitignore (or delete the file). Commit:

powershell
git add .
git commit -m "Ongeza class Maktaba na kuhifadhi JSON"

Step 5: main.py

python
"""Mfumo wa Maktaba - menu ya terminal."""
from pathlib import Path

from makosa import KosaLaMaktaba
from maktaba import Maktaba
from models import Kitabu, Mwanachama

DATA = Path(__file__).parent / "data.json"

MENU = """
========= MFUMO WA MAKTABA =========
1. Ongeza kitabu        6. Rudisha kitabu
2. Sajili mwanachama    7. Tafuta kitabu
3. Onyesha vitabu       8. Vitabu vilivyochelewa
4. Onyesha wanachama    9. Takwimu
5. Azima kitabu         0. Toka
"""


def soma_namba(ujumbe: str) -> int:
    while True:
        try:
            return int(input(ujumbe))
        except ValueError:
            print("Tafadhali andika namba.")


def onyesha(vitu) -> None:
    vitu = list(vitu)
    if not vitu:
        print("(hakuna)")
    for kitu in vitu:
        print(" ", kitu)


def main() -> None:
    maktaba = Maktaba.pakia(DATA)
    print(maktaba)
    while True:
        print(MENU)
        chaguo = input("Chagua: ").strip()
        try:
            if chaguo == "1":
                kitabu = Kitabu(
                    isbn=input("ISBN: ").strip(),
                    kichwa=input("Kichwa: ").strip(),
                    mwandishi=input("Mwandishi: ").strip(),
                    mwaka=soma_namba("Mwaka: "),
                )
                maktaba.ongeza_kitabu(kitabu)
                print("Kitabu kimeongezwa.")
            elif chaguo == "2":
                m = Mwanachama(input("Namba (mf. M001): ").strip(), input("Jina: ").strip())
                maktaba.sajili(m)
                print("Mwanachama amesajiliwa.")
            elif chaguo == "3":
                onyesha(maktaba.vitabu_vilivyopangwa())
            elif chaguo == "4":
                onyesha(maktaba._wanachama.values())
            elif chaguo == "5":
                maktaba.azima(input("ISBN: ").strip(), input("Namba ya mwanachama: ").strip())
                print("Kimeazimwa. Kirudishwe ndani ya siku 14.")
            elif chaguo == "6":
                faini = maktaba.rudisha(input("ISBN: ").strip())
                print("Kimerudishwa." + (f" Faini: TSh {faini:,}" if faini else ""))
            elif chaguo == "7":
                onyesha(maktaba.tafuta(input("Neno la kutafuta: ")))
            elif chaguo == "8":
                onyesha(f"{k.kichwa} - siku {s}" for k, s in maktaba.vilivyochelewa())
            elif chaguo == "9":
                print(maktaba)
            elif chaguo == "0":
                print("Kwaheri! Data imehifadhiwa.")
                break
            else:
                print("Chaguo si sahihi.")
        except KosaLaMaktaba as e:
            print("KOSA:", e)


if __name__ == "__main__":
    main()

Run python main.py; add 2 books, 1 member, borrow, try to borrow again (the error is displayed nicely, the program does not die), exit, run it again — the data is still there!

Note: maktaba._wanachama.values() in option 4 is touching an "internal" attribute — ask the student: "Which Week 6 rule does this break? How would we fix it?" (Answer: encapsulation; add a method wanachama() to Maktaba that returns a list.) It is a good holiday exercise.

Step 6: README.md and push

markdown
# Mfumo wa Maktaba

Mradi wa Mwezi 2 wa kozi ya Python. Programu ya terminal ya kusimamia vitabu,
wanachama na kuazima, ikihifadhi data kwenye JSON.

## Jinsi ya kuendesha
```
python main.py
```
Inahitaji Python 3.12+. Hakuna packages za nje.

## Nilichojifunza
- OOP: dataclasses, encapsulation, custom exceptions
- Kuhifadhi data kwa JSON na pathlib
- Comprehensions, lambda, generators na decorators
- Git & GitHub
powershell
git add .
git status            # hakikisha data.json na .venv HAZIMO
git commit -m "Ongeza menu ya main.py na README"

On GitHub: New repository → maktaba → Public → no README → Create:

powershell
git remote add origin https://github.com/JINA_LAKO/maktaba.git
git branch -M main
git push -u origin main

Open the repo in the browser — the README appears below the files. Congratulations! This is the first piece of work in the student's portfolio.

Questions to ask the student

  1. Why have we split the code into 4 files? — Each file has one responsibility; it is easy to read, test and change.
  2. What does Kitabu(**d) do? — It unpacks the dict into keyword arguments for the dataclass's __init__.
  3. How does the hifadhi_baadaye decorator help us? — It saves the data by itself after each successful change.
  4. Why is a single except KosaLaMaktaba enough in main.py? — All the library errors inherit from it.
  5. Why is data.json in .gitignore? — It is user data, not code.
  6. What is the fine for a book borrowed on Dec 1 and returned on Dec 20? — 19 days − 14 = 5 × 200 = TSh 1,000.
  7. When you close the program and open it again, where does the data come from? — Maktaba.pakia(DATA) reads data.json.

Common mistakes

Mistake Error message Fix
Running main.py from another folder ModuleNotFoundError: No module named 'makosa' cd maktaba first, then python main.py
Naming a file maktaba.py and a folder maktaba, then importing from the parent folder ImportError / wrong module Run it from inside the project folder.
The JSON has a key that is not a field (e.g. you renamed a field) TypeError: Kitabu.__init__() got an unexpected keyword argument 'title' Delete the old data.json or fix the keys.
Circular import: models.py imports maktaba.py and vice versa ImportError: cannot import name 'Kitabu' from partially initialized module 'models' models.py must not import maktaba.py; the flow should go one way: main → maktaba → models/makosa.
mwaka (year) was entered as a string Sorting by year gives TypeError: '<' not supported between instances of 'str' and 'int' Use soma_namba()
data.json has been uploaded to GitHub — git rm --cached data.json, add it to .gitignore, commit.
Push is rejected (the repo has a README) ! [rejected] main -> main (fetch first) git pull origin main --allow-unrelated-histories, then git push

In-class exercise

(Do as much as time allows; the rest is holiday work.)

Exercise 3.1: Add a method wanachama(self) -> list[Mwanachama] to Maktaba that returns the members sorted by name, and use it in main.py instead of maktaba._wanachama.

Exercise 3.2: Add a "Delete book" option — a book must not be deletable if it is borrowed (raise KimeshaAzimwa).

Exercise 3.3: Add statistics to option 9: the author with the most books (dict comprehension / max(key=...)).

Answer
python
# 3.1 -- ndani ya class Maktaba
def wanachama(self) -> list[Mwanachama]:
    return sorted(self._wanachama.values(), key=lambda m: m.jina.lower())

# main.py, chaguo 4:
#     onyesha(maktaba.wanachama())

# 3.2 -- ndani ya class Maktaba
@hifadhi_baadaye
def futa_kitabu(self, isbn: str) -> Kitabu:
    k = self.kitabu(isbn)
    if k.kimeazimwa:
        raise KimeshaAzimwa(f"Huwezi kufuta '{k.kichwa}' - kimeazimwa.")
    return self._vitabu.pop(isbn)

# main.py:
#     elif chaguo == "10":
#         k = maktaba.futa_kitabu(input("ISBN: ").strip())
#         print(f"Kimefutwa: {k.kichwa}")
# (na ongeza "10. Futa kitabu" kwenye MENU)

# 3.3 -- ndani ya class Maktaba
def mwandishi_bora(self) -> tuple[str, int] | None:
    if not self._vitabu:
        return None
    waandishi = {k.mwandishi for k in self._vitabu.values()}
    hesabu = {
        w: sum(1 for k in self._vitabu.values() if k.mwandishi == w)
        for w in waandishi
    }
    jina = max(hesabu, key=hesabu.get)
    return jina, hesabu[jina]

# main.py, chaguo 9:
#     print(maktaba)
#     bora = maktaba.mwandishi_bora()
#     if bora:
#         print(f"Mwandishi mwenye vitabu vingi: {bora[0]} ({bora[1]})")

Remember to do git add ., git commit -m "...", git push after each exercise.

Month 2 Assessment

Ask the student (orally) to explain in their own words: (1) class vs object, (2) try/except, (3) why venv, (4) what a decorator is, (5) git add → commit → push. Write in your notebook the topics they could not explain — they will be reviewed in Week 9.


Homework

(Assignment 1 is due in Session 2. Assignment 2 is due in Session 3. The holiday work — see below — is due: Session 1 of Week 9.)

Assignment 1 — Comprehensions (after Session 1): From a list of student dicts [{"jina": ..., "alama": [..]}, ...] (at least 5): (a) a dict of jina: wastani (name: average) using a dict comprehension, (b) the names of those with an average ≥ 60, sorted from highest, (c) a set of all the unique marks.

Assignment 2 — Project preparation (after Session 2): (a) A generator soma_csv(path) that gives each CSV row as a dict (use csv.DictReader). (b) A decorator thibitisha_chanya (validate positive) that raises ValueError if any numeric argument is negative. (c) Make sure Git and the GitHub account are ready, and that the python-kozi repo has at least 1 commit.

Holiday Week (no sessions)

Tell the student: "Rest, enjoy the festivities! If you have time, these are optional exercises."

  1. Finish exercises 3.1–3.3 of the library project and push to GitHub.
  2. Add to the library: saving a history of each loan (a list of dicts in JSON) and an option to display it.
  3. Review Weeks 5–8: rewrite BankAccount (Week 6) without looking, as a @dataclass with @property.
  4. Read the READMEs of 2 Python repos on GitHub (e.g. https://github.com/TheAlgorithms/Python) — look at how they organise their files.
  5. Thinking question: could the vilivyochelewa generator have been written as a list comprehension? What are the advantages/disadvantages?
Answer
python
# Kazi 1
wanafunzi = [
    {"jina": "Asha", "alama": [80, 90, 85]},
    {"jina": "Juma", "alama": [40, 55, 50]},
    {"jina": "Neema", "alama": [70, 65, 75]},
    {"jina": "Baraka", "alama": [60, 58, 62]},
    {"jina": "Zawadi", "alama": [90, 95, 100]},
]
wastani = {w["jina"]: sum(w["alama"]) / len(w["alama"]) for w in wanafunzi}
print(wastani)
# {'Asha': 85.0, 'Juma': 48.33..., 'Neema': 70.0, 'Baraka': 60.0, 'Zawadi': 95.0}

waliofaulu = sorted((j for j, a in wastani.items() if a >= 60),
                    key=lambda j: wastani[j], reverse=True)
print(waliofaulu)     # ['Zawadi', 'Asha', 'Neema', 'Baraka']

kipekee = {a for w in wanafunzi for a in w["alama"]}
print(sorted(kipekee))
python
# Kazi 2
import csv
import functools


def soma_csv(path):
    with open(path, newline="", encoding="utf-8") as f:
        for row in csv.DictReader(f):
            yield row


def thibitisha_chanya(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for thamani in list(args) + list(kwargs.values()):
            if isinstance(thamani, (int, float)) and thamani < 0:
                raise ValueError(f"{func.__name__}: thamani hasi hairuhusiwi ({thamani})")
        return func(*args, **kwargs)
    return wrapper


@thibitisha_chanya
def eneo_la_mstatili(urefu, upana):
    return urefu * upana

print(eneo_la_mstatili(3, 4))      # 12
try:
    eneo_la_mstatili(3, upana=-1)
except ValueError as e:
    print(e)                        # eneo_la_mstatili: thamani hasi hairuhusiwi (-1)

Holiday, question 5: Yes — [(k, s) for ...] would be possible, but it would need to calculate the days twice or use the walrus operator, and it would build a whole list. The generator is cleaner to read and uses less memory; the disadvantage is that it can only be looped over once.

If you are asked a question you don't know

End-of-week checklist

  • Writes list/dict/set comprehensions including if and if/else.
  • Uses lambda with sorted(key=...), max(key=...).
  • Knows map/filter return an iterator (uses list()).
  • Explains iter/next/StopIteration and writes a generator with yield.
  • Writes a decorator with *args, **kwargs and functools.wraps.
  • Has set git config (name, email) and uses init, status, add, commit, log.
  • Has a .gitignore with .venv/ and __pycache__/.
  • Has pushed a repo to GitHub without help.
  • The "Library System" project works (add, register, borrow, return, search, JSON remembers the data).
  • The project is on GitHub with a README.
  • Knows the optional tasks for the holiday week.