Contents

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 an AkauntiYaBenki (bank account) class yourself twice without looking, until you understand what self is. 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 self to the student.

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

  1. Explain class, object (instance), attribute and method in their own words.
  2. Write a class with __init__ and create several objects.
  3. Explain what self is and why it is the first parameter of every method.
  4. Tell class attributes and instance attributes apart.
  5. Protect data with _jina (convention) and @property / a setter with validation.
  6. Write __str__ and __repr__ so that objects print nicely.
  7. Build a small system (BankAccount, Duka) with two classes working together.

Session 1 — Classes, Objects, __init__ and self

Session flow

  1. Review of the Week 5 homework (word counter, marks book)
  2. Why OOP? The problem of many dicts + scattered functions
  3. First class, objects, attributes
  4. __init__
  5. Break
  6. Methods and self
  7. In-class exercise
  8. 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 write asha.salimia(), Python does Mwanafunzi.salimia(asha) — so self inside the method is asha. That is why every method has self as 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."
  • "self is 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, but self changes 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

python
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

python
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:

text
Asha Juma
<class '__main__.Mwanafunzi'>
False

Say: "Two different objects from one blueprint."

Step 3: __init__

python
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

python
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:

text
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

python
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

  1. What is the difference between a class and an object? — A class is the blueprint; an object is the real thing created from it.
  2. When is __init__ called? — By itself, each time a new object is created.
  3. In asha.salimia(), who is self? — asha.
  4. Why self.jina = jina and not just jina = jina? — jina = jina is a local variable that disappears when the function ends; self.jina is stored on the object.
  5. What is a method? — A function inside a class that works with the object's data.
  6. Why do asha.alama and juma.alama not 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
python
# 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

  1. Review of the homework (class Simu)
  2. Class attributes vs instance attributes
  3. Encapsulation: the _salio convention, why we protect data
  4. Break
  5. @property and a setter with validation
  6. In-class exercise
  7. 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:

python
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, so obj.__salio gives AttributeError. Rarely used; use a single _ in this course. Just mention it.

@property lets us write a method but use it like an attribute (without ()):

python
@property
def salio(self):          # getter: akaunti.salio
    return self._salio

And a setter lets us add validation when someone writes obj.x = thamani:

python
@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."
  • "_salio is like a 'Staff only' sign on a door. The door is not locked, but good people respect it."
  • "@property is 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

python
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:

text
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

python
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

python
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

python
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:

text
5500
Kosa: Salio halitoshi (salio: 5500).
Kosa: property 'salio' of 'Akaunti' object has no setter

Step 5: Setter with validation — mtu.py

python
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:

text
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

  1. Where is a class attribute written? — Inside the class, outside the methods.
  2. If you write a.shule = "X", does b.shule change? — No; an instance attribute is created for a only.
  3. Why Mwanafunzi.idadi += 1 and not self.idadi += 1? — self.idadi += 1 would create an instance attribute for each object, and the class count would not increase.
  4. What does _salio mean? — It is internal; do not touch it directly from outside (convention).
  5. What is the benefit of @property? — We read it like an attribute but keep control (read-only, or validation in the setter).
  6. Why should a setter not write self.umri = thamani? — It would call the setter again and again → RecursionError. Use self._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
python
# 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

  1. Review of the homework (class Joto)
  2. __str__ and __repr__
  3. Mini project A: complete BankAccount (transaction history) — live
  4. Break
  5. Mini project B: Duka (inventory) — two classes working together, the student writes, you guide
  6. Summary of the week — 7 OOP terms
  7. 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 by print(obj), str(obj), the f-string f"{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__, print will 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

python
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:

python
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:

text
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

python
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):

text
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):

python
# 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):

python
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:

text
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:

python
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

  1. What does print(obj) use? — __str__ (or __repr__ if there is no __str__).
  2. What does print([obj1, obj2]) use? — Each object's __repr__.
  3. Should __str__ have print inside or return? — return a string.
  4. In Duka, why is _bidhaa a dict and not a list? — Looking up by name is fast and easy.
  5. Why does hamisha (transfer) call self.toa (withdraw) first? — If the balance is not enough, an exception is raised and weka (deposit) is not called; money is not created out of thin air.
  6. 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
python
# 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
python
# 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()
python
# 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
python
# 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

End-of-week checklist

  • Explains class vs object with their own example.
  • Writes a class with __init__ and methods without forgetting self.
  • Explains who self is in obj.method().
  • Tells class attribute and instance attribute apart (and the obj.x = ... trap).
  • Knows why a list should not be a class attribute.
  • Uses _jina and @property (getter + setter with validation).
  • Writes __str__ and __repr__ that return a string.
  • Has built a BankAccount that raises an exception for insufficient balance.
  • Has built a Duka with Bidhaa objects (composition).
  • The Week 6 homework has been handed in.