DEV Community

Cover image for Python Basics in the Matatu Context
David Mwandairo
David Mwandairo

Posted on

Python Basics in the Matatu Context

Watch a matatu conductor work for one trip and you will see a programmer at full speed. He counts heads. He knows the fare. He takes a KSh 500 note from someone who owes KSh 70 and hands back the exact change without a pause. He also decides, by feel, when the vehicle is full enough to leave.

Listen to what he tracks: a number plate, a seat count, a fuel bill and a yes or no on whether the vehicle is full. That is text, a whole number, a decimal number and a true-or-false answer. Python cares about that difference, and once you see why, the basics stop looking like rules to memorise.

This article covers variables, four data types (string, integer, float and boolean), input and output, and the syntax that binds them together. Every example follows one fictional conductor working for the Mwananchi Express sacco. The fares, fuel prices and plate number are invented, so change them to match real life. Each code block shows the output you will see in your terminal.

Variables: names, not boxes

Most tutorials say a variable is a box that holds a value. The picture fails quickly, so use this one. A variable is a sticky note with a name on it. Python sticks the note onto a value. You can peel the note off and stick it on something else, and you can put two notes on one value.

plate = "KDD 482P"
seats = 14
fare_kes = 100
is_full = True

print(plate, seats, fare_kes, is_full)
Enter fullscreen mode Exit fullscreen mode
$ python matatu.py
KDD 482P 14 100 True
Enter fullscreen mode Exit fullscreen mode

The = sign does not mean "equals" the way it does in maths. It means "stick this name on that value." Python works out the right side first, then attaches the name on the left.

That order explains a line that would be nonsense in algebra. Say the sacco raises the fare by KSh 20:

fare_kes = fare_kes + 20
print(fare_kes)
Enter fullscreen mode Exit fullscreen mode
120
Enter fullscreen mode Exit fullscreen mode

Python takes the current value of fare_kes (100), adds 20, and moves the sticky note onto the result. Nothing was changed in place. The note moved.

Here is the two-notes idea at work:

x = 14
y = x        # y sits on the same value as x
x = 13       # x moves to a new value; y stays put
print(x, y)
Enter fullscreen mode Exit fullscreen mode
13 14
Enter fullscreen mode Exit fullscreen mode

Changing x did not touch y, because y never pointed at x. It pointed at the value 14.

Naming rules

A name can hold letters, digits and underscores. It cannot start with a digit, and it cannot be a word Python already uses, such as if or True. Names are case sensitive, so Seats and seats are two different notes.

Seats = 14
print(seats)
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "matatu.py", line 2, in <module>
    print(seats)
          ^^^^^
NameError: name 'seats' is not defined. Did you mean: 'Seats'?
Enter fullscreen mode Exit fullscreen mode

Read the error from the bottom. Python names the problem (NameError), names the culprit (seats), and guesses what you meant. Most Python errors help you this much once you learn to read them.

The community habit is snake_case: lowercase words joined by underscores. Put the unit in the name (fare_kes, fuel_litres) and you prevent a whole class of mistakes later. Values that should never change get capital letters, like SEATS. Python will let you change them anyway. The capitals are a note to the next reader.

The four data types

Every value has a type, and the type decides what you can do with it. Ask Python with type():

print(type(plate))
print(type(seats))
print(type(38.5))
print(type(is_full))
Enter fullscreen mode Exit fullscreen mode
<class 'str'>
<class 'int'>
<class 'float'>
<class 'bool'>
Enter fullscreen mode Exit fullscreen mode

Strings (str)

A string is text inside quotes. Single or double quotes both work. Pick one style and keep it.

sacco = "Mwananchi Express"

print(len(sacco))
print(sacco.upper())
print(sacco + " Sacco")
print(sacco[0])
print(sacco[:9])
print("Karibu! " * 2)
Enter fullscreen mode Exit fullscreen mode
17
MWANANCHI EXPRESS
Mwananchi Express Sacco
M
Mwananchi
Karibu! Karibu! 
Enter fullscreen mode Exit fullscreen mode

len() counts characters, spaces included. The + sign glues strings together. Counting starts at zero, so sacco[0] is the first letter, and sacco[:9] means "everything before position 9." Multiplying a string by a number repeats it.

Strings suit phone numbers well. A Safaricom number is text, not a number, because nobody adds two phone numbers together and the leading zero matters. Here is how to turn a local number into the international form that M-Pesa integrations expect:

local_number = "0712345678"

print("+254" + local_number[1:])
print(len(local_number))
print(local_number.startswith("07"))
Enter fullscreen mode Exit fullscreen mode
+254712345678
10
True
Enter fullscreen mode Exit fullscreen mode

The slice [1:] means "everything from position 1 onward," which drops the leading zero. The startswith() method asks a question about the text, and the answer comes back as a boolean. We will meet booleans shortly.

A string that looks like a number is still a string. "14" and 14 are different things, and Python refuses to mix them:

print("Seats: " + 14)
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "matatu.py", line 1, in <module>
    print("Seats: " + 14)
          ~~~~~~~~~~^~~~
TypeError: can only concatenate str (not "int") to str
Enter fullscreen mode Exit fullscreen mode

You have to convert the number first. We cover that in a moment.

Integers (int)

An integer is a whole number. Python has three division-related operators, and beginners mix them up:

riders = 47

print(riders // 14)
print(riders % 14)
print(2 ** 10)
print(7 / 2)
Enter fullscreen mode Exit fullscreen mode
3
5
1024
3.5
Enter fullscreen mode Exit fullscreen mode

The // operator divides and drops the remainder. A 14-seater carrying 47 riders in a day needs 3 full loads. The % operator keeps only the remainder: 5 riders are left for a fourth, partly empty trip. Together they answer "how many full loads, and what is left?", a question that turns up all the time. The ** operator raises a number to a power. The single / always returns a float, even when the answer is whole:

print(type(8 / 2), 8 / 2)
Enter fullscreen mode Exit fullscreen mode
<class 'float'> 4.0
Enter fullscreen mode Exit fullscreen mode

Floats (float)

A float is a number with a decimal point. Fuel is measured in litres and priced to the cent, so floats fit. Mixing an integer with a float gives a float:

print(3 * 1.5)
Enter fullscreen mode Exit fullscreen mode
4.5
Enter fullscreen mode Exit fullscreen mode

Floats have one quirk that startles everyone once:

print(0.1 + 0.2)
print(round(0.1 + 0.2, 2))
Enter fullscreen mode Exit fullscreen mode
0.30000000000000004
0.3
Enter fullscreen mode Exit fullscreen mode

Computers store numbers in binary. One tenth has no exact binary form, just as one third has no exact decimal form, so the machine stores a very close neighbour. The error is tiny, and it appears when you print raw results. Two habits protect you. Use round() or a format specifier when you display a float, and never test two floats for exact equality.

Booleans (bool)

A boolean is True or False, with a capital first letter. You will seldom type them yourself. Comparisons produce them:

boarded = 14
is_full = boarded >= seats
traffic_heavy = False

print(is_full)
print(is_full and not traffic_heavy)
Enter fullscreen mode Exit fullscreen mode
True
True
Enter fullscreen mode Exit fullscreen mode

The keywords and, or and not combine booleans. The expression is_full and not traffic_heavy asks: is the vehicle full, and is the road clear? Both hold, so the answer is True. Set traffic_heavy to True and the answer flips to False.

Converting between types

Python will not guess when you mix types, so you convert on purpose with int(), float(), str() and bool():

print(int("120") + 30)
print(float("38.5") * 2)
print(str(14) + " seats")
print(int(9.99))
Enter fullscreen mode Exit fullscreen mode
150
77.0
14 seats
9
Enter fullscreen mode Exit fullscreen mode

Notice that int(9.99) gives 9. Converting to an integer cuts off the decimals. It does not round.

Converting to a boolean has a trap:

print(bool(0), bool(1), bool(""), bool("no"))
print(bool("False"))
Enter fullscreen mode Exit fullscreen mode
False True False True
True
Enter fullscreen mode Exit fullscreen mode

bool() asks one question: is there anything here? Zero and the empty string count as nothing, so they become False. Everything else becomes True, including the text "False", which is a non-empty string. If you read the word "false" from a user, compare it with the text yourself. Do not pass it to bool().

Input and output

Python talks through print() and listens through input().

Output with print()

print() takes several values and puts a space between them. Two options change that: sep sets the separator, and end sets what closes the line (a newline by default).

print("Nairobi", "Thika", "Kenol", sep=" > ")
print("Counting heads...", end=" ")
print("done")
Enter fullscreen mode Exit fullscreen mode
Nairobi > Thika > Kenol
Counting heads... done
Enter fullscreen mode Exit fullscreen mode

For anything longer, use an f-string. Put an f before the opening quote and write expressions inside curly braces. After a colon you control the format:

fuel_cost = 5.5 * 190
print(f"{sacco} spent KSh {fuel_cost:.2f} on fuel")
print(f"KSh {1234567.891:,.2f}")
Enter fullscreen mode Exit fullscreen mode
Mwananchi Express spent KSh 1045.00 on fuel
KSh 1,234,567.89
Enter fullscreen mode Exit fullscreen mode

The .2f means "two digits after the decimal point," and the comma adds thousands separators. F-strings also fix the "Seats: " + 14 problem, because they convert values to text for you.

Input with input()

input() shows a prompt, waits for the user to type and press Enter, then hands you what they typed. In the transcripts below, the text after each prompt is what the user typed.

name = input("Conductor name: ")
print("Karibu,", name)
Enter fullscreen mode Exit fullscreen mode
Conductor name: Maxwell
Karibu, Maxwell
Enter fullscreen mode Exit fullscreen mode

Remember this rule: input() always returns a string. If the user types 3, you receive the text "3".

passengers = input("Passengers: ")
print(passengers + 1)
Enter fullscreen mode Exit fullscreen mode
Passengers: 3
Traceback (most recent call last):
  File "matatu.py", line 2, in <module>
    print(passengers + 1)
          ~~~~~~~~~~~^~~
TypeError: can only concatenate str (not "int") to str
Enter fullscreen mode Exit fullscreen mode

The fix is to convert the moment the value arrives: passengers = int(input("Passengers: ")). If the user types something that is not a number, int() raises a ValueError. Handling that needs try and except, which sit outside this article. For now, know that it can happen.

Syntax rules that hold it together

Python's syntax is small. Five rules cover most of what you will write this month.

Indentation is part of the language. Where other languages use braces, Python uses the four spaces at the start of a line to show what belongs inside an if. Leave them out and Python stops you:

seats = 14
if seats > 10:
print("big")
Enter fullscreen mode Exit fullscreen mode
  File "matatu.py", line 3
    print("big")
    ^
IndentationError: expected an indented block after 'if' statement on line 2
Enter fullscreen mode Exit fullscreen mode

A colon ends the line that opens a block (if, elif, else). Comments start with #, and Python ignores them, so use them to explain why you did something. A line break ends a statement, so you need no semicolons. And names are case sensitive, as you saw with Seats.

The if statement is the one piece of flow control we need. It runs a block only when a boolean is True. The second and third projects below use it.

Practical example 1: the conductor's change counter

A passenger owes KSh 70 and hands over a KSh 500 note. The conductor must return the right change, fast, and sometimes the passenger has paid too little. This program uses integers, division, a boolean and an if statement.

# change_counter.py
fare_kes = int(input("Fare (KSh): "))
paid_kes = int(input("Passenger paid (KSh): "))

enough = paid_kes >= fare_kes

if enough:
    change = paid_kes - fare_kes
    hundreds = change // 100
    small_change = change % 100
    print(f"Change due: KSh {change}")
    print(f"Give back: {hundreds} x KSh 100, plus KSh {small_change} in smaller notes or coins")
else:
    print(f"Short by KSh {fare_kes - paid_kes}. Ask for the rest.")
Enter fullscreen mode Exit fullscreen mode

Two runs, one where the passenger overpays and one where they come up short:

$ python change_counter.py
Fare (KSh): 70
Passenger paid (KSh): 500
Change due: KSh 430
Give back: 4 x KSh 100, plus KSh 30 in smaller notes or coins

$ python change_counter.py
Fare (KSh): 70
Passenger paid (KSh): 50
Short by KSh 20. Ask for the rest.
Enter fullscreen mode Exit fullscreen mode

Follow the arithmetic: 500 minus 70 is 430. Then 430 // 100 is 4, the number of hundred-shilling notes, and 430 % 100 is 30, what remains. The same // and % pair that counted full loads now counts notes.

Practical example 2: the trip profit calculator

At the end of a trip, the owner wants to know whether the trip paid for itself. Income is the passenger count times the fare. Costs are the fuel and a flat sacco fee. This program mixes integers and floats and ends on a boolean.

# trip_profit.py
FARE_KES = 100
SACCO_FEE_KES = 150

passengers = int(input("Passengers on this trip: "))
fuel_litres = float(input("Fuel used (litres): "))
price_per_litre = float(input("Fuel price (KSh per litre): "))

income = passengers * FARE_KES
fuel_cost = fuel_litres * price_per_litre
profit = income - fuel_cost - SACCO_FEE_KES
made_profit = profit > 0

print()
print(f"Income:    KSh {income:,.2f}")
print(f"Fuel:      KSh {fuel_cost:,.2f}")
print(f"Sacco fee: KSh {SACCO_FEE_KES:,.2f}")
print(f"Profit:    KSh {profit:,.2f}")
print("Trip paid for itself?", made_profit)
Enter fullscreen mode Exit fullscreen mode

A full vehicle on one run, and a half-empty one on another:

$ python trip_profit.py
Passengers on this trip: 14
Fuel used (litres): 5.5
Fuel price (KSh per litre): 190

Income:    KSh 1,400.00
Fuel:      KSh 1,045.00
Sacco fee: KSh 150.00
Profit:    KSh 205.00
Trip paid for itself? True

$ python trip_profit.py
Passengers on this trip: 9
Fuel used (litres): 5.5
Fuel price (KSh per litre): 190

Income:    KSh 900.00
Fuel:      KSh 1,045.00
Sacco fee: KSh 150.00
Profit:    KSh -295.00
Trip paid for itself? False
Enter fullscreen mode Exit fullscreen mode

The numbers show why conductors hate leaving half empty. Five fewer passengers turn a KSh 205 profit into a KSh 295 loss. Look at the types: passengers is an integer, fuel_litres is a float, and multiplying the integer by the integer constant gives an integer, while any product with a float gives a float. The :,.2f format makes every amount line up with two decimals and a thousands separator.

Practical example 3: the departure call

The hardest decision on the route is when to go. Leave too early and you lose fares. Wait too long and passengers get off. This program takes the number of people on board and the minutes spent waiting, turns them into three booleans, and decides.

# departure.py
SEATS = 14

boarded = int(input("Passengers on board: "))
minutes_waiting = int(input("Minutes waiting: "))

is_full = boarded >= SEATS
waited_long = minutes_waiting >= 20
half_full = boarded >= SEATS / 2

if is_full:
    print("Decision: full. Close the door and go.")
elif waited_long and half_full:
    print("Decision: enough people and enough waiting. Leave now.")
elif waited_long:
    print("Decision: long wait, few riders. Keep calling, but tell them the delay.")
else:
    print("Decision: keep calling for passengers.")
Enter fullscreen mode Exit fullscreen mode

Four situations, four decisions:

$ python departure.py
Passengers on board: 14
Minutes waiting: 5
Decision: full. Close the door and go.

$ python departure.py
Passengers on board: 9
Minutes waiting: 25
Decision: enough people and enough waiting. Leave now.

$ python departure.py
Passengers on board: 4
Minutes waiting: 30
Decision: long wait, few riders. Keep calling, but tell them the delay.

$ python departure.py
Passengers on board: 6
Minutes waiting: 8
Decision: keep calling for passengers.
Enter fullscreen mode Exit fullscreen mode

Python checks the if and elif lines from top to bottom and runs only the first block whose condition is true. That is why the full vehicle leaves even after five minutes: is_full is checked first. Notice that SEATS / 2 is a float (7.0), and comparing an integer with a float works without any conversion.

Try it yourself

You now have every piece for tools like these. Pick one and bend it:

  1. Extend the change counter to hand back KSh 50 notes as well as KSh 100 notes, using another // and % step.
  2. Change the profit calculator to ask for the fare, so it works for a route that charges KSh 60 or KSh 150.
  3. Add a fourth rule to the departure call: if the road is blocked, tell the passengers and stop counting.

Then pick a small problem from your own week. Splitting a shopping bill, converting shillings to dollars and tracking your data bundle all follow the same steps: ask for input, convert it, calculate, and print the answer. Each program you finish teaches you more than another hour of reading.

Top comments (0)