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)
- Install before Session 1 (on BOTH computers if possible, and on the student's):
- Git for Windows: https://git-scm.com/download/win (click Next for the defaults; choose "Visual Studio Code" as the default editor if asked).
- Confirm in PowerShell:
git --version(e.g.git version 2.47.0.windows.1). - A GitHub account (https://github.com) — you and the student. Tell the student to create it before Session 2 and to verify their email.
- Read:
- Comprehensions: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions
- Generators: https://docs.python.org/3/howto/functional.html#generators
- Decorators (glossary): https://docs.python.org/3/glossary.html#term-decorator
- GitHub "Hello World": https://docs.github.com/en/get-started/start-your-journey/hello-world
- The Pro Git book (chapters 1–2, free): https://git-scm.com/book/en/v2
- Do it yourself first: create a test repo,
commit, andpushto GitHub at least once before Session 2. The most common first error is about authentication (login) — you will know it yourself before class. - Project: run all of
maktaba(library, Session 3) before Session 3 so that you know every part. - Prepare a folder:
python_kozi\wiki08\.
Week Goals (by the end of the week the student will be able to...)
- Write list, dict and set comprehensions (including
if). - Use
lambdatogether withsorted(key=...),map,filter,max/min(key=...). - Explain what an iterator is (
iter,next,StopIteration) and write a generator withyield. - Write a simple decorator (including
*args, **kwargsandfunctools.wraps). - Use Git:
init,status,add,commit,log,.gitignore. - Use GitHub: create a repo,
remote add,push. - Build and upload to GitHub a library project that uses OOP + JSON + exceptions.
Session 1 — Comprehensions, lambda, map/filter/sorted
Session flow
- Review of the Week 7 homework (especially
Rafu— we will need it in Session 3) - List comprehensions (+
if, +if/else) - Dict and set comprehensions; a generator expression inside
sum() - Break
lambdasorted(key=...),max/min(key=...),map,filter- In-class exercise
- 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:
[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):
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."
- "
lambdais like writing on a small slip of paper (sticky note) — short, one-time instructions that do not need a whole notebook (def)." - "
key=is tellingsorted'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
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:
[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
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
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:
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
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
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
- What does
[x * 2 for x in range(5)]give? —[0, 2, 4, 6, 8]. - Where does a filtering
ifgo in a comprehension? — At the end, after thefor. - What does
{x for x in [1, 1, 2]}give? — The set{1, 2}. - Can a lambda have multiple lines? — No, one expression only.
- What does
key=lenmean insorted? — Sort by the length of each element. - Why does
print(map(...))show<map object>? —mapreturns an iterator; uselist(). - What is the difference between
sorted(lst)andlst.sort()? —sortedreturns a new list;.sort()changes the same list and returnsNone.
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
# 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
- Short review of the homework
- Iterators:
iter,next,StopIteration - Generators:
yield - Review of
*args,**kwargs - Decorators
- Break
- Git: config, init, status, add, commit, log,
.gitignore - GitHub: create a repo,
remote add,push - 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:
- Functions are objects — they can be passed as arguments (we have done this with
key=!). - A function can be defined inside another function and returned.
@decoratorabovedef fis shorthand forf = 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:
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):
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:
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 addis putting things in a box;git commitis sealing the box and writing a label;git pushis posting it."
Type this code live (share screen)
Step 1: Iterator — iterators.py
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
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:
<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:
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:
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
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:
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:
# (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
# (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:
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):
[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:
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:
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:
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:
git status # modified git diff # inaonyesha mistari iliyobadilika (+ kijani, - nyekundu) git add . git commit -m "Ongeza salamu ya pili" git log --oneline
Sample output:
b4e1c2a (HEAD -> main) Ongeza salamu ya pili 7a9d3f0 Ongeza programu ya salamu
.gitignore — create a venv and show the problem:
python -m venv .venv git status # .venv/ inaonekana -- hatutaki!
Create the file .gitignore (in VS Code: New File → .gitignore):
.venv/ __pycache__/ *.pyc .vscode/
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":
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
- What happens if you call
next()on an exhausted iterator? —StopIteration. - What is the difference between
returnandyield? —returnends the function;yieldgives a value and pauses, carrying on at the nextnext. - Why does a generator use little memory? — It does not build a whole list; it gives items one at a time.
- What type is
*argsinside a function?**kwargs? — Tuple; dict. @keleleabovedef agais the same as what? —aga = kelele(aga).- Why should a wrapper use
*args, **kwargs? — So that it works with any function, with any arguments. - What are the three steps for saving changes to GitHub? —
git add,git commit -m "...",git push. - Why should
.venv/be in.gitignore? — It is large and can be recreated fromrequirements.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
# 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:
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
- Review: did Git work? Is the
Rafufrom Week 7 ready? - Project explanation, file structure,
git init+.gitignore models.py— dataclassesKitabu,Mwanachama+ exceptions (makosa.py)maktaba.py— classMaktaba(logic) + saving/loading JSON- Break (commit first!)
main.py— terminal menu, handling errors- README, final commit,
git pushto GitHub - 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:
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)(fromdataclasses) → a dict for JSON;Kitabu(**d)→ turns a dict back into an object (this is where the**kwargsfrom 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:
modelsis the records department,maktabais the manager who makes decisions,mainis 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
mkdir maktaba cd maktaba git init code .gitignore
.gitignore:
.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
"""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
"""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:
python -c "from models import Kitabu; k = Kitabu('1', 'Kinjeketile', 'E. Hussein', 1969); print(k)"
Output: [1] Kinjeketile - E. Hussein (1969) | kipo
git add . git commit -m "Ongeza models na makosa"
Step 4: maktaba.py (the heart of the project)
"""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 callhifadhi()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):
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:
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:
git add . git commit -m "Ongeza class Maktaba na kuhifadhi JSON"
Step 5: main.py
"""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
# 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
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:
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
- Why have we split the code into 4 files? — Each file has one responsibility; it is easy to read, test and change.
- What does
Kitabu(**d)do? — It unpacks the dict into keyword arguments for the dataclass's__init__. - How does the
hifadhi_baadayedecorator help us? — It saves the data by itself after each successful change. - Why is a single
except KosaLaMaktabaenough inmain.py? — All the library errors inherit from it. - Why is
data.jsonin.gitignore? — It is user data, not code. - What is the fine for a book borrowed on Dec 1 and returned on Dec 20? — 19 days − 14 = 5 × 200 = TSh 1,000.
- When you close the program and open it again, where does the data come from? —
Maktaba.pakia(DATA)readsdata.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
# 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."
- Finish exercises 3.1–3.3 of the library project and push to GitHub.
- Add to the library: saving a history of each loan (a list of dicts in JSON) and an option to display it.
- Review Weeks 5–8: rewrite
BankAccount(Week 6) without looking, as a@dataclasswith@property. - Read the READMEs of 2 Python repos on GitHub (e.g. https://github.com/TheAlgorithms/Python) — look at how they organise their files.
- Thinking question: could the
vilivyochelewagenerator have been written as a list comprehension? What are the advantages/disadvantages?
Answer
# 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))
# 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
- Git is the part with the most unexpected questions. The rule: read the
git statusmessage — it often says what to do. Do not usegit push --forceorgit reset --hardin class without understanding them — they can delete work. If you get stuck, copy the error message and search for it word for word. - Docs: comprehensions https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions | sorting HOWTO https://docs.python.org/3/howto/sorting.html | generators https://docs.python.org/3/howto/functional.html |
functools.wrapshttps://docs.python.org/3/library/functools.html#functools.wraps | Git book https://git-scm.com/book/en/v2 | GitHub docs https://docs.github.com/en/get-started |.gitignoretemplates https://github.com/github/gitignore/blob/main/Python.gitignore - Say: "Even senior developers look up Git commands every day — knowing how to search is a skill."
End-of-week checklist
- Writes list/dict/set comprehensions including
ifandif/else. - Uses
lambdawithsorted(key=...),max(key=...). - Knows
map/filterreturn an iterator (useslist()). - Explains
iter/next/StopIterationand writes a generator withyield. - Writes a decorator with
*args, **kwargsandfunctools.wraps. - Has set
git config(name, email) and usesinit,status,add,commit,log. - Has a
.gitignorewith.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.