Week 6: OOP Part 1 — Classes and Objects
Month 2 | Main goal: The student understands and uses classes, objects,
__init__,self, methods, class vs instance attributes, encapsulation (_private,@property) and__str__/__repr__, and builds a mini project of a BankAccount and a shop (Duka).
Teacher Preparation (read before the week starts)
- Read: Python Tutorial — "Classes", sections 9.1–9.4 only: https://docs.python.org/3/tutorial/classes.html . Do not worry about the inheritance/iterators sections — those are Weeks 7–8.
- Personal practice: Write a
Mbwa(dog) class and anAkauntiYaBenki(bank account) class yourself twice without looking, until you understand whatselfis. Run every example from "Type this code live". - The key idea to remember: A class is a blueprint; an object is the real thing built from that blueprint. Once you understand this, everything else follows.
- Prepare a folder:
python_kozi\wiki06\. There is no new package to install. - Use Python Tutor (optional): https://pythontutor.com — it shows objects as diagrams; good for showing
selfto the student.
Week Goals (by the end of the week the student will be able to...)
- Explain class, object (instance), attribute and method in their own words.
- Write a class with
__init__and create several objects. - Explain what
selfis and why it is the first parameter of every method. - Tell class attributes and instance attributes apart.
- Protect data with
_jina(convention) and@property/ a setter with validation. - Write
__str__and__repr__so that objects print nicely. - Build a small system (BankAccount,
Duka) with two classes working together.
Session 1 — Classes, Objects, __init__ and self
Session flow
- Review of the Week 5 homework (word counter, marks book)
- Why OOP? The problem of many dicts + scattered functions
- First class, objects, attributes
__init__- Break
- Methods and
self - In-class exercise
- Q&A + homework
Explanation for the teacher (read this first)
OOP (Object-Oriented Programming) is a style of organising code in which we combine related data (attributes) and behaviour (methods) inside one thing — an object.
Remember Week 5: we had a student dict {"jina": "Asha", "alama": 88} and separate functions such as ongeza_alama(mwanafunzi, n). That works, but as the program grows: dicts can have mistyped keys ("jna"), and there is no single place that says "what a student has and what a student can do". A class solves that.
Key terms:
- class — a blueprint/template. The name usually starts with a capital letter:
Mwanafunzi(student),AkauntiYaBenki(PascalCase). - object / instance — a real thing created from a class:
asha = Mwanafunzi("Asha"). - attribute — an object's data:
asha.jina. - method — a function inside a class:
asha.salimia()(greet). __init__— a special method called by itself when an object is created. Its job: to set the initial attributes. (It is not a full "constructor" in the strict technical sense, but it is fine to call it that in class.)self— the object itself. When you writeasha.salimia(), Python doesMwanafunzi.salimia(asha)— soselfinside the method isasha. That is why every method hasselfas its first parameter, but we do not pass it ourselves when calling.
The name self is not a keyword — it is only a convention, but always use self.
Say this to the student
- "A class is the house plan a builder has. One plan can build 100 houses. Each house (object) has its own colour and its own owner, but they are all built from one plan."
- "Think of a school enrolment form. The blank form is the class. Each filled-in form — Asha's, Juma's — is an object. The sections of the form (name, age) are attributes."
- "
selfis the word 'me'. When Asha says 'my name is...' she is talking about herself. When Juma says the same words, he is talking about Juma. One method, butselfchanges depending on who calls it." - "
__init__is like the day of birth: the baby is given a name and a date of birth right then."
Type this code live (share screen)
Step 1: The problem — kwa_nini_oop.py
asha = {"jina": "Asha", "alama": 88} juma = {"jna": "Juma", "alama": 70} # kosa la kuandika! Python haitusaidii def salimia(mwanafunzi): print("Habari, mimi ni", mwanafunzi["jina"]) salimia(asha) salimia(juma) # KeyError: 'jina'
Say: "A dict does not know what a student is supposed to have. We will create a 'blueprint'."
Step 2: Empty class and attributes — class_ya_kwanza.py
class Mwanafunzi: pass a = Mwanafunzi() b = Mwanafunzi() a.jina = "Asha" b.jina = "Juma" print(a.jina, b.jina) print(type(a)) print(a is b)
Output:
Asha Juma <class '__main__.Mwanafunzi'> False
Say: "Two different objects from one blueprint."
Step 3: __init__
class Mwanafunzi: def __init__(self, jina, alama): self.jina = jina self.alama = alama asha = Mwanafunzi("Asha", 88) juma = Mwanafunzi("Juma", 70) print(asha.jina, asha.alama) print(juma.jina, juma.alama)
Explain by drawing (or in words): Mwanafunzi("Asha", 88) → Python creates an empty object → it calls __init__(object_huo, "Asha", 88) → self.jina = "Asha" sets the attribute on that object → the object is returned and stored in asha.
Show the error: Mwanafunzi("Asha") → TypeError: Mwanafunzi.__init__() missing 1 required positional argument: 'alama'. "Now Python is protecting us!"
Step 4: Methods
class Mwanafunzi: def __init__(self, jina, alama=0): self.jina = jina self.alama = alama def salimia(self): print(f"Habari, mimi ni {self.jina}.") def ongeza_alama(self, kiasi): self.alama += kiasi def amefaulu(self): return self.alama >= 50 asha = Mwanafunzi("Asha", 45) asha.salimia() print(asha.amefaulu()) asha.ongeza_alama(10) print(asha.alama, asha.amefaulu()) juma = Mwanafunzi("Juma") juma.salimia() print(juma.alama)
Output:
Habari, mimi ni Asha. False 55 True Habari, mimi ni Juma. 0
Show that Mwanafunzi.salimia(asha) gives the same result as asha.salimia() — "this is the secret of self".
Step 5: Objects in a list
darasa = [Mwanafunzi("Asha", 88), Mwanafunzi("Juma", 42), Mwanafunzi("Neema", 67)] for m in darasa: hali = "amefaulu" if m.amefaulu() else "hajafaulu" print(f"{m.jina}: {m.alama} - {hali}")
Questions to ask the student
- What is the difference between a class and an object? — A class is the blueprint; an object is the real thing created from it.
- When is
__init__called? — By itself, each time a new object is created. - In
asha.salimia(), who isself? —asha. - Why
self.jina = jinaand not justjina = jina? —jina = jinais a local variable that disappears when the function ends;self.jinais stored on the object. - What is a method? — A function inside a class that works with the object's data.
- Why do
asha.alamaandjuma.alamanot get mixed up? — Each object has its own attributes.
Common mistakes
| Mistake | Error message | Fix |
|---|---|---|
Forgetting self in a method |
TypeError: Mwanafunzi.salimia() takes 0 positional arguments but 1 was given |
def salimia(self): |
Forgetting self. inside a method |
NameError: name 'jina' is not defined |
self.jina |
Writing __init or _init_ (one underscore) |
TypeError: Mwanafunzi() takes no arguments |
Two underscores on each side: __init__ |
Calling a method without () |
Prints <bound method Mwanafunzi.salimia of ...> |
asha.salimia() |
Passing self yourself: asha.salimia(asha) |
TypeError: ... takes 1 positional argument but 2 were given |
Do not pass self; Python does it. |
| Indentation: method outside the class | NameError or the method does not exist: AttributeError: 'Mwanafunzi' object has no attribute 'salimia' |
Methods must be inside (indented in) the class. |
In-class exercise
Exercise 1.1: Create a class Gari (car) with attributes aina (type), rangi (colour), kasi (speed, default 0). Methods: ongeza_kasi(kiasi) (speed up), punguza_kasi(kiasi) (slow down; speed must not go below 0), taarifa() (report), which prints the state of the car.
Exercise 1.2: Create a class Mstatili (rectangle) with urefu (length) and upana (width), and methods eneo() (area) and mzunguko() (perimeter). Create 3 rectangles in a list, print the area of each one and the largest one.
Exercise 1.3: Class Kikapu (shopping basket): __init__ starts with an empty list self.vitu. Methods: ongeza(jina, bei) (add), jumla() (total), onyesha() (show).
Answer
# 1.1 class Gari: def __init__(self, aina, rangi, kasi=0): self.aina = aina self.rangi = rangi self.kasi = kasi def ongeza_kasi(self, kiasi): self.kasi += kiasi def punguza_kasi(self, kiasi): self.kasi = max(0, self.kasi - kiasi) def taarifa(self): print(f"{self.aina} ya rangi {self.rangi} inaenda {self.kasi} km/h") g = Gari("Toyota IST", "nyeupe") g.ongeza_kasi(60) g.punguza_kasi(100) g.taarifa() # Toyota IST ya rangi nyeupe inaenda 0 km/h # 1.2 class Mstatili: def __init__(self, urefu, upana): self.urefu = urefu self.upana = upana def eneo(self): return self.urefu * self.upana def mzunguko(self): return 2 * (self.urefu + self.upana) mistatili = [Mstatili(2, 3), Mstatili(5, 4), Mstatili(1, 10)] kubwa = mistatili[0] for m in mistatili: print(f"{m.urefu}x{m.upana}: eneo={m.eneo()}, mzunguko={m.mzunguko()}") if m.eneo() > kubwa.eneo(): kubwa = m print("Kubwa zaidi:", kubwa.urefu, "x", kubwa.upana) # 5 x 4 # 1.3 class Kikapu: def __init__(self): self.vitu = [] def ongeza(self, jina, bei): self.vitu.append((jina, bei)) def jumla(self): total = 0 for _, bei in self.vitu: total += bei return total def onyesha(self): for jina, bei in self.vitu: print(f"{jina:<10}{bei:>8,}") print(f"{'JUMLA':<10}{self.jumla():>8,}") k = Kikapu() k.ongeza("Sukari", 3000) k.ongeza("Mafuta", 7500) k.onyesha()
Session 2 — Class vs Instance Attributes, Encapsulation and @property
Session flow
- Review of the homework (class
Simu) - Class attributes vs instance attributes
- Encapsulation: the
_salioconvention, why we protect data - Break
@propertyand a setter with validation- In-class exercise
- Q&A + homework
Explanation for the teacher (read this first)
Instance attribute — set with self.x = ... (usually inside __init__), and it belongs to one object only.
Class attribute — set directly inside the class (outside any method) and shared by all objects:
class Mwanafunzi: shule = "Python School" # class attribute idadi = 0 # class attribute def __init__(self, jina): self.jina = jina # instance attribute Mwanafunzi.idadi += 1
When Python looks up obj.x: it checks the object first, then the class. Trap: asha.shule = "Nyingine" does NOT change the class attribute — it creates a new instance attribute for asha only, which hides the class one. To change it for everyone: Mwanafunzi.shule = "...". For that reason, in __init__ we write Mwanafunzi.idadi += 1, not self.idadi += 1.
An even bigger trap: do not make a list/dict a class attribute if you want each object to have its own — they will all share one list!
Encapsulation = hiding internal details and controlling how data is changed. Example: a bank balance must not be negative; nobody should write akaunti.salio = -1000000.
Python does not have full private like Java. It has conventions:
_salio(one underscore) — "this is internal, do not touch it from outside". It is still possible, but it is an agreement among code writers.__salio(two underscores, without two at the end) — "name mangling": Python renames it to_AkauntiYaBenki__salio, soobj.__saliogivesAttributeError. Rarely used; use a single_in this course. Just mention it.
@property lets us write a method but use it like an attribute (without ()):
@property def salio(self): # getter: akaunti.salio return self._salio
And a setter lets us add validation when someone writes obj.x = thamani:
@salio.setter def salio(self, thamani): if thamani < 0: raise ValueError(...) self._salio = thamani
Without a setter, a property is read-only: obj.salio = 5 → AttributeError: property 'salio' of 'Akaunti' object has no setter.
A property is also good for computed values, e.g. the eneo (area) of a rectangle: m.eneo instead of m.eneo().
@property is a "decorator" — we will learn decorators fully in Week 8. For now: "it is a marker that changes a function's behaviour."
Say this to the student
- "A class attribute is like the school name on the uniform — all students have it. An instance attribute is the student's name — each has their own."
- "Encapsulation is like an ATM. You cannot open the machine and take the money yourself; you use the buttons (methods), which check the PIN and balance first."
- "
_saliois like a 'Staff only' sign on a door. The door is not locked, but good people respect it." - "
@propertyis like a bank teller: you ask 'what is my balance?' and they check the internal ledger and answer you. If you want to change it, it has to go through the rules."
Type this code live (share screen)
Step 1: Class attributes — class_attr.py
class Mwanafunzi: shule = "Python School" idadi = 0 def __init__(self, jina): self.jina = jina Mwanafunzi.idadi += 1 a = Mwanafunzi("Asha") b = Mwanafunzi("Juma") print(a.shule, b.shule, Mwanafunzi.shule) print("Idadi:", Mwanafunzi.idadi) Mwanafunzi.shule = "Python Academy" print(a.shule, b.shule) a.shule = "Shule Binafsi" # mtego! print(a.shule, b.shule, Mwanafunzi.shule) print(a.__dict__) print(b.__dict__)
Output:
Python School Python School Python School
Idadi: 2
Python Academy Python Academy
Shule Binafsi Python Academy Python Academy
{'jina': 'Asha', 'shule': 'Shule Binafsi'}
{'jina': 'Juma'}
Explain __dict__ — "it shows the attributes inside the object itself."
Step 2: The trap of a list as a class attribute
class Kikapu: vitu = [] # MBAYA: list moja kwa wote def ongeza(self, kitu): self.vitu.append(kitu) k1 = Kikapu() k2 = Kikapu() k1.ongeza("embe") print(k2.vitu) # ['embe'] -- k2 hakununua chochote!
Fix it by putting self.vitu = [] inside __init__.
Step 3: The problem without encapsulation — akaunti.py
class Akaunti: def __init__(self, mmiliki, salio=0): self.mmiliki = mmiliki self.salio = salio a = Akaunti("Asha", 5000) a.salio = -1_000_000 # hakuna anayezuia! print(a.salio)
Step 4: _salio + methods + @property
class Akaunti: def __init__(self, mmiliki, salio=0): self.mmiliki = mmiliki self._salio = 0 self.weka(salio) # tumia validation hata mwanzoni @property def salio(self): return self._salio def weka(self, kiasi): if kiasi < 0: raise ValueError("Kiasi cha kuweka hakiwezi kuwa hasi.") self._salio += kiasi def toa(self, kiasi): if kiasi <= 0: raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.") if kiasi > self._salio: raise ValueError(f"Salio halitoshi (salio: {self._salio}).") self._salio -= kiasi a = Akaunti("Asha", 5000) a.weka(2000) a.toa(1500) print(a.salio) # 5500 -- bila () try: a.toa(100_000) except ValueError as e: print("Kosa:", e) try: a.salio = 999_999 except AttributeError as e: print("Kosa:", e)
Output:
5500 Kosa: Salio halitoshi (salio: 5500). Kosa: property 'salio' of 'Akaunti' object has no setter
Step 5: Setter with validation — mtu.py
class Mtu: def __init__(self, jina, umri): self.jina = jina self.umri = umri # hii inaita setter hapa chini! @property def umri(self): return self._umri @umri.setter def umri(self, thamani): if not isinstance(thamani, int) or not 0 <= thamani <= 130: raise ValueError(f"Umri si sahihi: {thamani!r}") self._umri = thamani @property def ni_mtu_mzima(self): # computed, read-only return self.umri >= 18 m = Mtu("Juma", 17) print(m.umri, m.ni_mtu_mzima) m.umri = 18 print(m.umri, m.ni_mtu_mzima) try: m.umri = -3 except ValueError as e: print(e) try: Mtu("Neema", "kumi") except ValueError as e: print(e)
Output:
17 False 18 True Umri si sahihi: -3 Umri si sahihi: 'kumi'
Explain !r — it shows the value using repr, which is why 'kumi' has quotes.
Questions to ask the student
- Where is a class attribute written? — Inside the class, outside the methods.
- If you write
a.shule = "X", doesb.shulechange? — No; an instance attribute is created foraonly. - Why
Mwanafunzi.idadi += 1and notself.idadi += 1? —self.idadi += 1would create an instance attribute for each object, and the class count would not increase. - What does
_saliomean? — It is internal; do not touch it directly from outside (convention). - What is the benefit of
@property? — We read it like an attribute but keep control (read-only, or validation in the setter). - Why should a setter not write
self.umri = thamani? — It would call the setter again and again →RecursionError. Useself._umri.
Common mistakes
| Mistake | Error message | Fix |
|---|---|---|
The setter assigns to itself self.umri = thamani |
RecursionError: maximum recursion depth exceeded |
self._umri = thamani |
Calling a property like a method a.salio() |
TypeError: 'int' object is not callable |
a.salio without () |
| Assigning a value to a property with no setter | AttributeError: property 'salio' of 'Akaunti' object has no setter |
Use a method (weka) or add a setter. |
| The setter's name differs from the property's | AttributeError or the property is lost |
Both must have the same name: @umri.setter def umri(...) |
| A list as a class attribute | All objects share the data | self.vitu = [] inside __init__ |
Using __salio and trying to read it from outside |
AttributeError: 'Akaunti' object has no attribute '__salio' |
Use a single _salio + a property. |
In-class exercise
Exercise 2.1: Class Bidhaa (product) with a class attribute VAT = 0.18 and instance attributes jina (name), bei (price). A read-only property bei_na_vat (price with VAT). Then change Bidhaa.VAT = 0.16 and see that all products change.
Exercise 2.2: Class Mstatili with properties urefu and upana (setters must not allow ≤ 0 → ValueError), and a computed property eneo.
Exercise 2.3: Class Mwanafunzi that counts how many students have been created (idadi) and has a property alama (0–100 only).
Answer
# 2.1 class Bidhaa: VAT = 0.18 def __init__(self, jina, bei): self.jina = jina self.bei = bei @property def bei_na_vat(self): return round(self.bei * (1 + Bidhaa.VAT)) s = Bidhaa("Sukari", 3000) m = Bidhaa("Mchele", 2500) print(s.bei_na_vat, m.bei_na_vat) # 3540 2950 Bidhaa.VAT = 0.16 print(s.bei_na_vat, m.bei_na_vat) # 3480 2900 # 2.2 class Mstatili: def __init__(self, urefu, upana): self.urefu = urefu self.upana = upana @property def urefu(self): return self._urefu @urefu.setter def urefu(self, v): if v <= 0: raise ValueError("Urefu lazima uwe > 0") self._urefu = v @property def upana(self): return self._upana @upana.setter def upana(self, v): if v <= 0: raise ValueError("Upana lazima uwe > 0") self._upana = v @property def eneo(self): return self.urefu * self.upana r = Mstatili(3, 4) print(r.eneo) # 12 r.upana = 10 print(r.eneo) # 30 try: r.urefu = 0 except ValueError as e: print(e) # Urefu lazima uwe > 0 # 2.3 class Mwanafunzi: idadi = 0 def __init__(self, jina, alama=0): self.jina = jina self.alama = alama Mwanafunzi.idadi += 1 @property def alama(self): return self._alama @alama.setter def alama(self, v): if not 0 <= v <= 100: raise ValueError("Alama ni 0 hadi 100") self._alama = v Mwanafunzi("Asha", 80) Mwanafunzi("Juma") print(Mwanafunzi.idadi) # 2
Session 3 — __str__, __repr__ and Mini Project: BankAccount and Duka
Session flow
- Review of the homework (class
Joto) __str__and__repr__- Mini project A: complete
BankAccount(transaction history) — live - Break
- Mini project B:
Duka(inventory) — two classes working together, the student writes, you guide - Summary of the week — 7 OOP terms
- Q&A + homework
Explanation for the teacher (read this first)
If you print an object without any preparation: print(a) → <__main__.Akaunti object at 0x0000021F...> — that means nothing to a user. We can teach Python how to display an object using dunder methods ("double underscore", e.g. __init__). In Week 7 we will see more dunders.
__str__(self)— "nice" text for the user. Used byprint(obj),str(obj), the f-stringf"{obj}".__repr__(self)— "technical" text for the developer, ideally looking like code that could create that object again:Akaunti('Asha', 5000). Used by the REPL, the debugger, and inside a list:print([a, b])uses each item's__repr__!- If you only have
__repr__,printwill use it too. So if you are only going to write one, write__repr__. - Both must return a string, not
print.
Mini projects: the goal is to combine everything from the week — __init__, methods, _private, @property, exceptions (Week 5), __str__, and objects inside objects (a Duka holds many Bidhaa in a dict). Remember: a Duka has products — this is called composition ("has-a"). Next week we will see inheritance ("is-a").
Say this to the student
- "
__str__is the business card you give a customer — nice and short.__repr__is the national ID (NIDA) — full details for the office." - "A shop has many products — like a cupboard full of jars. The cupboard is one object (
Duka), each jar is an object (Bidhaa)."
Type this code live (share screen)
Step 1: Without and with __str__/__repr__ — onyesha.py
class Bidhaa: def __init__(self, jina, bei): self.jina = jina self.bei = bei b = Bidhaa("Sukari", 3000) print(b) # <__main__.Bidhaa object at 0x...>
Add:
class Bidhaa: def __init__(self, jina, bei): self.jina = jina self.bei = bei def __str__(self): return f"{self.jina} - TSh {self.bei:,}" def __repr__(self): return f"Bidhaa({self.jina!r}, {self.bei!r})" b = Bidhaa("Sukari", 3000) print(b) print(str(b)) print(repr(b)) print(f"Nimenunua {b}") print([b, Bidhaa("Chumvi", 800)])
Output:
Sukari - TSh 3,000
Sukari - TSh 3,000
Bidhaa('Sukari', 3000)
Nimenunua Sukari - TSh 3,000
[Bidhaa('Sukari', 3000), Bidhaa('Chumvi', 800)]
Emphasise the last line: the list uses __repr__.
Step 2: Mini project A — BankAccount — bank_account.py
from datetime import datetime class SalioHalitoshi(Exception): pass class BankAccount: benki = "Benki ya Python" _namba_inayofuata = 1001 def __init__(self, mmiliki, salio_la_mwanzo=0): self.mmiliki = mmiliki self.namba = BankAccount._namba_inayofuata BankAccount._namba_inayofuata += 1 self._salio = 0 self._miamala = [] if salio_la_mwanzo > 0: self.weka(salio_la_mwanzo) @property def salio(self): return self._salio def _rekodi(self, aina, kiasi): wakati = datetime.now().strftime("%Y-%m-%d %H:%M") self._miamala.append((wakati, aina, kiasi, self._salio)) def weka(self, kiasi): if kiasi <= 0: raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.") self._salio += kiasi self._rekodi("WEKA", kiasi) def toa(self, kiasi): if kiasi <= 0: raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.") if kiasi > self._salio: raise SalioHalitoshi( f"Unataka {kiasi:,} lakini salio ni {self._salio:,}." ) self._salio -= kiasi self._rekodi("TOA", kiasi) def hamisha(self, akaunti_nyingine, kiasi): self.toa(kiasi) # kama inashindwa, haiendelei akaunti_nyingine.weka(kiasi) def taarifa(self): print(f"--- Taarifa ya {self.mmiliki} ({self.namba}) ---") for wakati, aina, kiasi, salio in self._miamala: print(f"{wakati} {aina:<5}{kiasi:>10,} salio: {salio:>10,}") def __str__(self): return f"{self.mmiliki} ({self.namba}): TSh {self._salio:,}" def __repr__(self): return f"BankAccount({self.mmiliki!r}, {self._salio!r})" if __name__ == "__main__": asha = BankAccount("Asha", 50_000) juma = BankAccount("Juma") asha.toa(10_000) asha.hamisha(juma, 15_000) print(asha) print(juma) try: juma.toa(1_000_000) except SalioHalitoshi as e: print("Imeshindikana:", e) asha.taarifa()
Output (the date will differ):
Asha (1001): TSh 25,000 Juma (1002): TSh 15,000 Imeshindikana: Unataka 1,000,000 lakini salio ni 15,000. --- Taarifa ya Asha (1001) --- 2026-12-11 10:15 WEKA 50,000 salio: 50,000 2026-12-11 10:15 TOA 10,000 salio: 40,000 2026-12-11 10:15 TOA 15,000 salio: 25,000
Explain if __name__ == "__main__": (review of Week 4 — modules): this code runs only when we run this file directly, not when we import it.
Step 3: Mini project B — Duka. Have the student write it while you direct them step by step. Give them the structure first (write it as comments):
# Bidhaa: jina, bei, idadi; property thamani (bei*idadi); __str__ # Duka: jina, dict ya bidhaa (jina -> Bidhaa) # ongeza_bidhaa(bidhaa), uza(jina, idadi) -> inarudisha bei ya mauzo # ongeza_stock(jina, idadi), bidhaa_zinazoisha(kikomo=5), thamani_ya_stock() # mapato (property)
Solution (duka.py):
class BidhaaHaipo(Exception): pass class StockHaitoshi(Exception): pass class Bidhaa: def __init__(self, jina, bei, idadi=0): if bei <= 0: raise ValueError("Bei lazima iwe zaidi ya sifuri.") self.jina = jina self.bei = bei self.idadi = idadi @property def thamani(self): return self.bei * self.idadi def __str__(self): return f"{self.jina:<10} bei {self.bei:>7,} idadi {self.idadi:>4}" def __repr__(self): return f"Bidhaa({self.jina!r}, {self.bei!r}, {self.idadi!r})" class Duka: def __init__(self, jina): self.jina = jina self._bidhaa = {} self._mapato = 0 @property def mapato(self): return self._mapato def ongeza_bidhaa(self, bidhaa): self._bidhaa[bidhaa.jina.lower()] = bidhaa def _tafuta(self, jina): try: return self._bidhaa[jina.lower()] except KeyError: raise BidhaaHaipo(f"'{jina}' haipo dukani.") from None def ongeza_stock(self, jina, idadi): self._tafuta(jina).idadi += idadi def uza(self, jina, idadi): b = self._tafuta(jina) if idadi > b.idadi: raise StockHaitoshi(f"Zimebaki {b.idadi} tu za {b.jina}.") b.idadi -= idadi jumla = b.bei * idadi self._mapato += jumla return jumla def bidhaa_zinazoisha(self, kikomo=5): return [b for b in self._bidhaa.values() if b.idadi <= kikomo] def thamani_ya_stock(self): return sum(b.thamani for b in self._bidhaa.values()) def __str__(self): mistari = [f"=== {self.jina} ==="] for b in self._bidhaa.values(): mistari.append(str(b)) mistari.append(f"Thamani ya stock: TSh {self.thamani_ya_stock():,}") mistari.append(f"Mapato: TSh {self._mapato:,}") return "\n".join(mistari) if __name__ == "__main__": duka = Duka("Duka la Mama Neema") duka.ongeza_bidhaa(Bidhaa("Sukari", 3000, 20)) duka.ongeza_bidhaa(Bidhaa("Mchele", 2500, 6)) duka.ongeza_bidhaa(Bidhaa("Sabuni", 1200, 3)) print("Mauzo:", duka.uza("sukari", 5)) for jina, n in [("Mchele", 10), ("Unga", 1)]: try: duka.uza(jina, n) except (StockHaitoshi, BidhaaHaipo) as e: print("Kosa:", e) print(duka) print("Zinaisha:", duka.bidhaa_zinazoisha())
Output:
Mauzo: 15000
Kosa: Zimebaki 6 tu za Mchele.
Kosa: 'Unga' haipo dukani.
=== Duka la Mama Neema ===
Sukari bei 3,000 idadi 15
Mchele bei 2,500 idadi 6
Sabuni bei 1,200 idadi 3
Thamani ya stock: TSh 63,600
Mapato: TSh 15,000
Zinaisha: [Bidhaa('Sabuni', 1200, 3)]
Notes for the teacher: list comprehension ([b for b in ...]) and sum(... for ...) belong to Week 8. If the student is not used to them, show the loop version:
def bidhaa_zinazoisha(self, kikomo=5): matokeo = [] for b in self._bidhaa.values(): if b.idadi <= kikomo: matokeo.append(b) return matokeo
raise ... from None hides the inner KeyError so that the traceback is clean — just say "it's tidiness".
Why is "Mchele" (rice, 6) not under "Zinaisha" (running out)? Because the limit is 5 and 6 <= 5 is False — ask this as a thinking question.
Questions to ask the student
- What does
print(obj)use? —__str__(or__repr__if there is no__str__). - What does
print([obj1, obj2])use? — Each object's__repr__. - Should
__str__haveprintinside orreturn? —returna string. - In
Duka, why is_bidhaaa dict and not a list? — Looking up by name is fast and easy. - Why does
hamisha(transfer) callself.toa(withdraw) first? — If the balance is not enough, an exception is raised andweka(deposit) is not called; money is not created out of thin air. - A shop "has" products — what is this called? — Composition (has-a).
Common mistakes
| Mistake | Error message | Fix |
|---|---|---|
__str__ does print instead of return |
TypeError: __str__ returned non-string (type NoneType) |
return f"..." |
__str__ returns a number |
TypeError: __str__ returned non-string (type int) |
Return a string. |
Forgetting to return the value in uza (sell) |
print(duka.uza(...)) shows None |
return jumla |
Adding the object instead of looking up by name: duka._bidhaa[bidhaa] |
KeyError later |
Use bidhaa.jina as the key. |
Forgetting self._mapato = 0 in __init__ |
AttributeError: 'Duka' object has no attribute '_mapato' |
Initialise all attributes in __init__. |
In-class exercise
Exercise 3.1: Add __str__ and __repr__ to the Gari class from Session 1.
Exercise 3.2: Add a method ripoti_ya_mauzo() (sales report) to Duka — the shop should keep a record of each sale (jina, idadi, jumla — name, quantity, total) in a list, and the method should print them all.
Exercise 3.3: Add a daily withdrawal limit to BankAccount (class attribute KIKOMO_CHA_SIKU = 200_000). If someone tries to withdraw a total above the limit, raise ValueError. (For simplicity, calculate the total of all of today's "TOA" (withdraw) entries in _miamala.)
Answer
# 3.1 class Gari: def __init__(self, aina, rangi, kasi=0): self.aina = aina self.rangi = rangi self.kasi = kasi def __str__(self): return f"{self.aina} ({self.rangi}) - {self.kasi} km/h" def __repr__(self): return f"Gari({self.aina!r}, {self.rangi!r}, {self.kasi!r})" # 3.2 -- mabadiliko kwenye Duka # __init__: self._mauzo = [] # uza(): baada ya kuhesabu jumla: # self._mauzo.append((b.jina, idadi, jumla)) def ripoti_ya_mauzo(self): print("--- Mauzo ---") for jina, idadi, jumla in self._mauzo: print(f"{jina:<10}{idadi:>4} x = {jumla:>8,}") print(f"Jumla ya mapato: {self._mapato:,}") # 3.3 -- mabadiliko kwenye BankAccount from datetime import datetime class BankAccount: KIKOMO_CHA_SIKU = 200_000 # ... (sehemu nyingine kama zilivyo) def _iliyotolewa_leo(self): leo = datetime.now().strftime("%Y-%m-%d") jumla = 0 for wakati, aina, kiasi, _ in self._miamala: if aina == "TOA" and wakati.startswith(leo): jumla += kiasi return jumla def toa(self, kiasi): if kiasi <= 0: raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.") if self._iliyotolewa_leo() + kiasi > BankAccount.KIKOMO_CHA_SIKU: raise ValueError("Umezidi kikomo cha kutoa kwa siku.") if kiasi > self._salio: raise SalioHalitoshi(f"Salio ni {self._salio:,}.") self._salio -= kiasi self._rekodi("TOA", kiasi)
Homework
(Due: next session)
Assignment 1 (after Session 1): Class Simu (phone) with chapa (brand), modeli (model), chaji (charge, default 100). Methods: piga_simu(dakika) (make a call, by minutes) — each minute reduces the charge by 2% (the charge must not go below 0; if it is not enough, print "Chaji haitoshi" — not enough charge), chaji_simu(asilimia) (charge the phone, by percentage; must not exceed 100), hali() (status).
Assignment 2 (after Session 2): Class Joto (temperature) that stores _celsius. A property celsius with a setter (must not accept below -273.15), and a property fahrenheit with a getter and a setter (F = C × 9/5 + 32).
Assignment 3 (after Session 3): Save Duka to JSON! Add a method hifadhi(path) (save) to Duka, and an ordinary function (outside the class) pakia_duka(path) (load shop) that returns a new Duka from JSON. Add __repr__ to Duka. The program should have a small menu: add product, sell, show, exit (saving before exiting).
Answer
# Kazi 1 class Simu: def __init__(self, chapa, modeli, chaji=100): self.chapa = chapa self.modeli = modeli self.chaji = chaji def piga_simu(self, dakika): inahitajika = dakika * 2 if inahitajika > self.chaji: print("Chaji haitoshi") return self.chaji -= inahitajika print(f"Umeongea dakika {dakika}.") def chaji_simu(self, asilimia): self.chaji = min(100, self.chaji + asilimia) def hali(self): print(f"{self.chapa} {self.modeli}: chaji {self.chaji}%") s = Simu("Tecno", "Spark 20") s.piga_simu(30) # chaji 40 s.piga_simu(25) # Chaji haitoshi s.chaji_simu(80) # 100 s.hali()
# Kazi 2 class Joto: def __init__(self, celsius=0): self.celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, v): if v < -273.15: raise ValueError("Chini ya absolute zero haiwezekani.") self._celsius = v @property def fahrenheit(self): return self._celsius * 9 / 5 + 32 @fahrenheit.setter def fahrenheit(self, f): self.celsius = (f - 32) * 5 / 9 # inapita validation ya celsius j = Joto(25) print(j.fahrenheit) # 77.0 j.fahrenheit = 212 print(j.celsius) # 100.0
# Kazi 3 -- weka chini ya classes za duka.py (Bidhaa, Duka, exceptions) import json from pathlib import Path # Ndani ya class Duka ongeza: # def __repr__(self): # return f"Duka({self.jina!r}, bidhaa={len(self._bidhaa)})" # # def hifadhi(self, path): # data = { # "jina": self.jina, # "mapato": self._mapato, # "bidhaa": [ # {"jina": b.jina, "bei": b.bei, "idadi": b.idadi} # for b in self._bidhaa.values() # ], # } # with open(path, "w", encoding="utf-8") as f: # json.dump(data, f, indent=2, ensure_ascii=False) def pakia_duka(path): with open(path, encoding="utf-8") as f: data = json.load(f) duka = Duka(data["jina"]) duka._mapato = data.get("mapato", 0) for b in data["bidhaa"]: duka.ongeza_bidhaa(Bidhaa(b["jina"], b["bei"], b["idadi"])) return duka FAILI = Path(__file__).parent / "duka.json" try: duka = pakia_duka(FAILI) except FileNotFoundError: duka = Duka("Duka Langu") while True: chaguo = input("\n1.Ongeza 2.Uza 3.Onyesha 4.Toka : ") try: if chaguo == "1": jina = input("Jina: ") bei = int(input("Bei: ")) idadi = int(input("Idadi: ")) duka.ongeza_bidhaa(Bidhaa(jina, bei, idadi)) elif chaguo == "2": jina = input("Jina: ") idadi = int(input("Idadi: ")) print("Jumla:", duka.uza(jina, idadi)) elif chaguo == "3": print(duka) elif chaguo == "4": duka.hifadhi(FAILI) print("Imehifadhiwa. Kwaheri!") break except (ValueError, BidhaaHaipo, StockHaitoshi) as e: print("Kosa:", e)
Note: @classmethod (the "official" way of creating an object from JSON) will be mentioned in Week 7/8 in the library project; for now an ordinary function is enough.
If you are asked a question you don't know
- It is fine to say "I'm not sure, let's try it." Open the REPL (
python) and try:dir(obj)shows all attributes/methods,vars(obj)orobj.__dict__shows the object's attributes,help(ClassName)shows a description. - Search: "python class property setter example", "python str vs repr". Docs: https://docs.python.org/3/tutorial/classes.html |
property: https://docs.python.org/3/library/functions.html#property | data model (dunders): https://docs.python.org/3/reference/datamodel.html#basic-customization - Python Tutor (https://pythontutor.com) is a good help for showing
selfand objects as pictures.
End-of-week checklist
- Explains class vs object with their own example.
- Writes a class with
__init__and methods without forgettingself. - Explains who
selfis inobj.method(). - Tells class attribute and instance attribute apart (and the
obj.x = ...trap). - Knows why a list should not be a class attribute.
- Uses
_jinaand@property(getter + setter with validation). - Writes
__str__and__repr__that return a string. - Has built a
BankAccountthat raises an exception for insufficient balance. - Has built a
DukawithBidhaaobjects (composition). - The Week 6 homework has been handed in.