Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Inhoud

  1. Kennismaking
  2. Invoer en uitvoer
  3. Selecties
  4. Lussen en iteraties
  5. Lijsten
  6. Functies
  7. Foutafhandeling
  8. Bestanden
  9. Modules & venv
  10. Basis testen en debuggen
  11. Eindproject

Antwoorden — Hoofdstuk 1: Variabelen en basisberekeningen

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven.

Antwoord 1 — Cirkel: omtrek en oppervlakte

  • We lezen een float, gebruiken math.pi, en tonen resultaten met f-strings.
import math

raw = input("Enter radius: ")
radius = float(raw)

circumference = 2 * math.pi * radius
area = math.pi * (radius ** 2)

print(f"Circumference: {circumference}")
print(f"Area: {area}")

Met afronding (optioneel):

print(f"Circumference: {circumference:.2f}")
print(f"Area: {area:.2f}")

Antwoord 2 — BMI met afronding

  • We vragen lengte en gewicht; ronden af op 1 decimaal in de uitvoer.
height_m = float(input("Enter height in meters: "))
weight_kg = float(input("Enter weight in kg: "))

bmi = weight_kg / (height_m ** 2)
print(f"BMI is {bmi:.1f}")

Antwoord 3 — BTW 9%

  • We gebruiken een variabele voor het percentage en ronden 2 decimalen.
amount = float(input("Amount (ex VAT): "))
vat_rate = 0.09
price_inc_vat = amount * (1 + vat_rate)
print(f"Price incl VAT: {price_inc_vat:.2f}")

Antwoord 4 — Variabelen ruilen (swap)

  • Twee varianten: met tijdelijke variabele, en met tuple-swap.
# With a temporary variable
a = 3
b = 7
print("Before:", a, b)

tmp = a
a = b
b = tmp
print("After:", a, b)
# Pythonic tuple swap
a = 3
b = 7
print("Before:", a, b)

a, b = b, a
print("After:", a, b)

Antwoord 5 — Kortingen stapelen

  • Eerst 10% en dan 5% is niet exact gelijk aan één keer 15%.
original = 80.0

first = original * 0.90   # 10% off
second = first * 0.95     # then 5% off

single = original * 0.85  # single 15% off

print(f"Stacked discounts: {second:.2f}")
print(f"Single 15%: {single:.2f}")
print("Equal?", abs(second - single) < 1e-9)

Antwoord 6 — Kommagetallen en precisie

  • 0.1 + 0.2 geeft een benadering; we tonen ook afgerond.
print(0.1 + 0.2)          # likely 0.30000000000000004
print((0.1 + 0.2) == 0.3) # False
print(round(0.1 + 0.2, 2))

Uitdaging (optioneel) — Rechthoek helper

width = float(input("Width: "))
height = float(input("Height: "))

area = width * height
perimeter = 2 * (width + height)

print(f"Area: {area:.2f}")
print(f"Perimeter: {perimeter:.2f}")

Antwoorden — Hoofdstuk 2: Invoer en uitvoer

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — Begroeting

  • We trimmen spaties met .strip() en gebruiken een f-string.
first = input("First name: ").strip()
print(f"Hello, {first}!")

Antwoord 2 — Volledige naam

first = input("First name: ").strip()
last = input("Last name: ").strip()
print(f"{last}, {first}")
print(f"{first} {last}")

Antwoord 3 — Bedrag netjes tonen (komma of punt)

raw = input("Amount: ").strip().replace(",", ".")
amount = float(raw)
print(f"Amount: {amount:.2f}")

Antwoord 4 — Percentage format

rate = float(input("Rate (e.g. 0.075): ").strip().replace(",", "."))
print(f"Rate: {rate:.1%}")

Antwoord 5 — Kleine bon (uitlijning)

name = input("Item name: ").strip()
qty = int(input("Quantity: ").strip())
price = float(input("Unit price: ").strip().replace(",", "."))

total = qty * price
print(f"{'Item':<10}{'Qty':>5}{'Price':>8}{'Total':>8}")
print(f"{name:<10}{qty:>5}{price:>8.2f}{total:>8.2f}")

Antwoord 6 — Print parameters

print("a", "b", "c", sep=", ", end="!")

Antwoord 7 — Voortgangspunten

  • Twee varianten: vijf losse prints of een kleine lus.
# Simple repeated prints
print(".", end="", flush=True)
print(".", end="", flush=True)
print(".", end="", flush=True)
print(".", end="", flush=True)
print(".", end="", flush=True)
# With a small loop (acceptable here)
for _ in range(5):
    print(".", end="", flush=True)

Antwoord 8 — Foutmelding via stderr

import sys
raw = input("Enter integer: ").strip()
try:
    n = int(raw)
    print(f"OK: {n}")
except ValueError:
    sys.stderr.write("Warning: not an integer\n")

Antwoord 9 — Unicode check

# Expect to see the characters rendered correctly in a UTF-8 capable terminal
print("café • naïve • € • λ")

Antwoord 10 — Command-line argument (teaser)

import sys
name = sys.argv[1] if len(sys.argv) > 1 else "world"
print(f"Hello, {name}!")

Uitdaging (optioneel) — Totaal met BTW en korting

raw_amount = input("Amount: ").strip().replace(",", ".")
amount = float(raw_amount)
raw_discount = input("Discount (0-100): ").strip().replace(",", ".")
discount_pct = float(raw_discount)

after_discount = amount * (1 - discount_pct / 100)
vat_rate = 0.21
vat_value = after_discount * vat_rate
final_total = after_discount + vat_value

print(f"Amount:         {amount:>8.2f}")
print(f"Discount:       {discount_pct:>8.1f}%")
print(f"After discount: {after_discount:>8.2f}")
print(f"VAT (21%):      {vat_value:>8.2f}")
print(f"Total:          {final_total:>8.2f}")

Antwoorden — Hoofdstuk 3: Selecties (if / elif / else)

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — Basis if

  • Deelbaarheid door 2 bepaalt even/odd.
n = int(input("Enter an integer: "))
if n % 2 == 0:
    print("even")
else:
    print("odd")

Antwoord 2 — Leeftijdscategorieën

  • Volgorde van drempels is belangrijk: strengste eerst of van hoog naar laag.
age = int(input("Age: "))
if age < 18:
    print("minor")
elif age <= 64:
    print("adult")
else:
    print("senior")

Antwoord 3 — Rapportcijfer naar label

  • Zorg dat intervallen elkaar niet overlappen en samen 0..10 afdekken.
score = float(input("Score (0-10): "))
if score < 5.5:
    label = "insufficient"
elif score < 7.0:
    label = "sufficient"
elif score < 8.5:
    label = "good"
else:
    label = "excellent"
print(label)

Antwoord 4 — Login-indicatie

  • Eenvoudige en/conditie; geen echte beveiliging.
username = input("Username: ")
password = input("Password: ")
if username == "admin" and password == "secret":
    print("Welcome!")
else:
    print("Access denied")

Antwoord 5 — Korting met lidmaatschap

  • Volgorde: eerst de strengere (lid + bedrag), dan de alternatieve drempel.
amount = float(input("Amount: "))
member = input("Member? (yes/no): ").strip().lower() == "yes"
if member and amount >= 100:
    discount = 0.10
elif amount >= 150:
    discount = 0.05
else:
    discount = 0.0
final_price = amount * (1 - discount)
print(f"Discount: {discount*100:.0f}%")
print(f"Final price: {final_price:.2f}")

Antwoord 6 — Truthiness en invoer (ternary)

query = input("Search query: ").strip()
msg = "Empty query" if not query else f"Searching for '{query}'..."
print(msg)

Antwoord 7 — Kettingvergelijking 10..20

x = int(input("x: "))
print(10 <= x <= 20)

Antwoord 8 — Guard clause (functie)

  • Vroeg stoppen bij ongeldige weight_kg en dan per land beslissen.
def shipping_cost(country: str, weight_kg: float) -> float:
    if weight_kg <= 0:
        raise ValueError("Weight must be positive")
    if country == "NL":
        return 4.50 if weight_kg <= 2 else 6.75
    elif country == "BE":
        return 5.20 if weight_kg <= 2 else 7.40
    else:
        return 9.90

if __name__ == "__main__":
    print(shipping_cost("NL", 1.5))  # 4.50
    print(shipping_cost("BE", 3.0))  # 7.40
    print(shipping_cost("DE", 0.5))  # 9.90

Antwoord 9 — Wachtwoordsterkte-indicatie (simpel)

pwd = input("Password: ")
length = len(pwd)
if length < 6:
    label = "weak"
elif length <= 9:
    label = "medium"
else:
    label = "strong"
print(f"{label} (length={length})")

Antwoord 10 — Netto prijs met btw‑optie

amount = float(input("Amount: "))
mode = input("Type (incl/excl): ").strip().lower()
VAT = 0.21
if mode == "incl":
    net = amount / (1 + VAT)
    print(f"Net (ex VAT): {net:.2f}")
elif mode == "excl":
    gross = amount * (1 + VAT)
    print(f"Gross (incl VAT): {gross:.2f}")
else:
    print("Unknown type; use 'incl' or 'excl'.")

Uitdaging (optioneel) — Kalenderdag naar type dag

day = input("Day name: ").strip().lower()
weekend = {"saturday", "sunday"}
label = "weekend" if day in weekend else "weekday"
print(label)

Antwoorden — Hoofdstuk 4: Lussen en iteraties

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — Tellen met range

  • range(1, 11) geeft 1..10 (stop is exclusief). We bouwen één regel met end=" ".
for i in range(1, 11):
    print(i, end=" ")
print()  # newline

Antwoord 2 — Som 1..n (accumulatie)

n = int(input("n: "))
total = 0
for i in range(1, n + 1):
    total += i
print(total)

Antwoord 3 — While-teller omlaag

n = 5
while n >= 1:
    print(n)
    n -= 1
print("Liftoff!")

Antwoord 4 — Alleen even getallen (continue)

n = 0
while n < 20:
    n += 1
    if n % 2 != 0:
        continue
    print(n)

Antwoord 5 — Eerste veelvoud vinden (for-else)

  • Bij break slaan we het else-deel over; zonder break draait else.
for i in range(1, 51):
    if i % 7 == 0 and i % 5 == 0:
        print("Found:", i)
        break
else:
    print("No match")

Antwoord 6 — Genummerde lijst (enumerate)

names = ["Ava", "Bo", "Cid", "Dia"]
for idx, name in enumerate(names, start=1):
    print(f"{idx}. {name}")

Antwoord 7 — Itereren over dict

user = {"name": "Liam", "age": 31, "city": "Rotterdam"}
for key, value in user.items():
    print(f"{key}: {value}")

Antwoord 8 — Tafel van vermenigvuldiging (genest)

for a in range(1, 11):
    for b in range(1, 11):
        print(f"{a*b:>4}", end="")
    print()

Antwoord 9 — Invoer tot leeg (sentinel)

count = 0
while True:
    line = input().strip()
    if line == "":
        break
    count += 1
print(f"Count: {count}")

Antwoord 10 — Filter en transformeer

nums = [3, 8, 1, 7, 2, 10, 5]

# filter evens
evens: list[int] = []
for x in nums:
    if x % 2 == 0:
        evens.append(x)

# transform squares
squares: list[int] = []
for x in nums:
    squares.append(x * x)

print(evens)
print(squares)

Uitdaging (optioneel) — FizzBuzz 1..100

for i in range(1, 101):
    if i % 15 == 0:
        print("FizzBuzz")
    elif i % 3 == 0:
        print("Fizz")
    elif i % 5 == 0:
        print("Buzz")
    else:
        print(i)

Antwoorden — Hoofdstuk 5: Lijsten (list)

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — Basis maken en lezen

fruits = ["apple", "banana", "cherry"]
print(len(fruits))
print(fruits[0])     # first
print(fruits[-1])    # last via negative index

Antwoord 2 — Slices

nums = [10, 20, 30, 40, 50, 60]
print(nums[1:4])   # [20, 30, 40]
print(nums[:3])    # [10, 20, 30]
print(nums[3:])    # [40, 50, 60]
rev = nums[::-1]
print(rev)

Antwoord 3 — Mutatie met index en slice

items = [1, 2, 3, 4, 5]
items[1] = 20
print(items)  # [1, 20, 3, 4, 5]

items[2:4] = [7, 8, 9]   # replaces 3,4 by 7,8,9
print(items)  # [1, 20, 7, 8, 9, 5]

items[2:2] = [100, 101]  # insert at position 2
print(items)  # [1, 20, 100, 101, 7, 8, 9, 5]

Antwoord 4 — Toevoegen en verwijderen

colors = ["red"]
colors.append("green")
colors.extend(["blue", "green"])  # now ["red","green","blue","green"]
colors.remove("green")              # removes first "green"
last = colors.pop()                  # removes last element
print("popped:", last)
print(colors)

Antwoord 5 — Zoeken en membership

letters = ["a", "b", "a", "c", "a"]
print("b" in letters)        # True
print(letters.index("a"))   # 0
print(letters.count("a"))   # 3

Antwoord 6 — Iteratie en enumerate

names = ["Ava", "Bo", "Cid"]
for n in names:
    print(n)

for i, n in enumerate(names, start=1):
    print(f"{i}. {n}")

Antwoord 7 — Sorteren met key en reverse

names = ["bo", "Ava", "cid", "Dia"]
# case-insensitive alphabetical
case_ins = sorted(names, key=str.lower)
print(case_ins)

# by length desc, then alphabetical (case-insensitive) for ties
by_len_desc = sorted(names, key=lambda s: (len(s), s.lower()), reverse=True)
print(by_len_desc)

Antwoord 8 — Kopiëren vs. aliasing

original = [1, 2, 3]
alias = original          # no copy
alias.append(4)
print(original, alias)    # both show [1,2,3,4]

copy = original[:]        # shallow copy
copy.append(5)
print(original)  # [1, 2, 3, 4]
print(copy)      # [1, 2, 3, 4, 5]

Antwoord 9 — 2D‑lijst: vermenigvuldigingsvalkuil

# wrong way (rows alias the same inner list)
m = [[0] * 3] * 2
m[0][0] = 1
print(m)  # [[1, 0, 0], [1, 0, 0]]

# correct way (independent rows)
rows = 2; cols = 3
m = [[0 for _ in range(cols)] for _ in range(rows)]
m[0][0] = 1
print(m)  # [[1, 0, 0], [0, 0, 0]]

Antwoord 10 — Lijstcomprehensies

squares = [x * x for x in range(11)]
print(squares)

evens = [x for x in range(21) if x % 2 == 0]
print(evens)

labels = ["even" if x % 2 == 0 else "odd" for x in range(6)]
print(labels)

Uitdaging (optioneel) — Unieke elementen met behoud van volgorde

def unique_in_order(items):
    seen = set()
    result = []
    for x in items:
        if x not in seen:
            seen.add(x)
            result.append(x)
    return result

print(unique_in_order(["a", "b", "a", "c", "b"]))  # ['a', 'b', 'c']

Antwoorden — Hoofdstuk 6: Functies en modulair denken

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — Begroeting als functie

from typing import Final

def greet(name: str) -> str:
    return f"Hello, {name}!"

if __name__ == "__main__":
    print(greet("Ava"))
    print(greet("Bo"))

Antwoord 2 — Oppervlakte cirkel (docstring + hints)

import math

def circle_area(radius: float) -> float:
    """Return circle area given a radius.
    - Uses math.pi.
    - radius must be >= 0 (negative radii don't make physical sense).
    """
    if radius < 0:
        raise ValueError("radius must be >= 0")
    return math.pi * (radius ** 2)

if __name__ == "__main__":
    for r in (0.0, 1.5, 2.0):
        print(r, circle_area(r))

Antwoord 3 — Standaard exponent

def power(base: float, exp: int = 2) -> float:
    return base ** exp

print(power(3))            # 9 (default exp=2)
print(power(3, 3))         # 27
print(power(base=2, exp=5))# 32

Antwoord 4 — Vroege return (guard)

def safe_div(a: float, b: float) -> float | None:
    if b == 0:
        return None
    return a / b

print(safe_div(10, 2))  # 5.0
print(safe_div(3, 0))   # None

Antwoord 5 — Meerdere waarden (min, max, avg)

  • We kiezen voor raise ValueError bij lege input en documenteren dat.
from typing import Iterable, Tuple

def min_max_avg(values: list[float]) -> tuple[float, float, float]:
    """Return (min, max, avg) for non-empty list of floats.
    Raises ValueError if values is empty.
    """
    if not values:
        raise ValueError("values must not be empty")
    return min(values), max(values), sum(values) / len(values)

print(min_max_avg([3.0, 8.0, 2.0, 7.0]))

Antwoord 6 — Invoer-helper zonder print in kernfunctie

def parse_float(text: str) -> float | None:
    try:
        return float(text.strip().replace(",", "."))
    except ValueError:
        return None

if __name__ == "__main__":
    raw = input("Enter a number: ")
    value = parse_float(raw)
    if value is None:
        print("Please enter a valid number (use digits, comma or dot).")
    else:
        print(f"You entered: {value}")

Antwoord 7 — Muteren vs. kopiëren

def append_exclamation_inplace(items: list[str]) -> None:
    items.append("!")

def with_exclamation_copy(items: list[str]) -> list[str]:
    result = list(items)
    result.append("!")
    return result

if __name__ == "__main__":
    words = ["hello"]
    append_exclamation_inplace(words)
    print("after inplace:", words)         # mutated
    words2 = ["hello"]
    out = with_exclamation_copy(words2)
    print("original:", words2)              # unchanged
    print("returned:", out)

Antwoord 8 — Scope (LEGB) demonstratie

x = 10  # global

def demo() -> None:
    x = 5  # local shadows global x inside this function
    print("inside demo:", x)

if __name__ == "__main__":
    demo()
    print("outside:", x)
    # Opmerking: binnen demo() zien we de lokale x=5; buiten blijft x=10.

Antwoord 9 — Docstring en help

def slugify(text: str) -> str:
    """Convert text to a lowercase, dash-separated slug.

    Example:
        >>> slugify("Hello World!")
        'hello-world'
    """
    return "-".join(
        "".join(ch.lower() for ch in text if ch.isalnum() or ch.isspace()).split()
    )

if __name__ == "__main__":
    print(slugify.__doc__)
    help(slugify)  # rendering depends on environment
    print(slugify("Hello World!"))

Antwoord 10 — Mutabel standaardargument (slecht vs. goed)

# BAD: default list is created once and reused across calls

def add_item_bad(item, bucket=[]):
    bucket.append(item)
    return bucket

print(add_item_bad(1))  # [1]
print(add_item_bad(2))  # [1, 2]  <-- unexpected accumulation across calls

# GOOD: use None sentinel and create a new list inside

def add_item(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(add_item(1))  # [1]
print(add_item(2))  # [2]  -- independent default per call

Uitdaging (optioneel) — Refactor naar functies

from typing import Iterable

def line_total(qty: int, price: float) -> float:
    return qty * price

def format_line(name: str, qty: int, price: float) -> str:
    total = line_total(qty, price)
    return f"{name:<12}{qty:>4}{price:>9.2f}{total:>9.2f}"

def grand_total(items: list[tuple[str, int, float]]) -> float:
    return sum(line_total(q, p) for _, q, p in items)

def render_receipt(items: list[tuple[str, int, float]]) -> str:
    lines = [f"{'Item':<12}{'Qty':>4}{'Price':>9}{'Total':>9}"]
    for name, qty, price in items:
        lines.append(format_line(name, qty, price))
    lines.append(f"{'Grand total:':<25}{grand_total(items):>9.2f}")
    return "\n".join(lines)

if __name__ == "__main__":
    items = [("apple", 3, 0.60), ("banana", 2, 0.50), ("cherry", 10, 0.20)]
    print(render_receipt(items))

Antwoorden — Hoofdstuk 7: Foutafhandeling (exceptions)

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — safe_int helper

def safe_int(text: str, default: int | None = None) -> int | None:
    try:
        return int(text)
    except ValueError:
        return default

if __name__ == "__main__":
    print(safe_int("123"))        # 123
    print(safe_int("abc"))        # None
    print(safe_int("abc", 0))     # 0

Antwoord 2 — read_float met komma

def read_float(prompt: str) -> float:
    while True:
        raw = input(prompt).strip().replace(",", ".")
        try:
            return float(raw)
        except ValueError:
            print("Please enter a valid number (digits, comma or dot).")
  • Blijft vragen tot het lukt; geen exception lekt naar de aanroeper.

Antwoord 3 — Calculator met ^ en nette fouten

def calc(a: float, op: str, b: float) -> float:
    if op == "+":
        return a + b
    if op == "-":
        return a - b
    if op == "*":
        return a * b
    if op == "/":
        if b == 0:
            raise ZeroDivisionError("division by zero")
        return a / b
    if op == "^":
        return a ** b
    raise ValueError(f"unknown operator: {op}")

if __name__ == "__main__":
    try:
        a = float(input("a: ").strip().replace(",", "."))
        op = input("op (+ - * / ^): ").strip()
        b = float(input("b: ").strip().replace(",", "."))
        print("=", calc(a, op, b))
    except ZeroDivisionError as e:
        print("Error:", e)
    except ValueError as e:
        print("Error:", e)

Antwoord 4 — else/finally en with

# Variant met try/except/else/finally
f = None
try:
    f = open("notes.txt", "r", encoding="utf-8")
    text = f.read()
except FileNotFoundError:
    print("File not found")
else:
    print("Length:", len(text))
finally:
    if f is not None:
        f.close()
    print("Done")

# Equivalent met contextmanager
try:
    with open("notes.txt", "r", encoding="utf-8") as f:
        text = f.read()
    print("Length:", len(text))  # only runs if open+read succeeded
except FileNotFoundError:
    print("File not found")
print("Done")

Antwoord 5 — ConfigError en load_config

from pathlib import Path

class ConfigError(Exception):
    """Raised when configuration is invalid or missing."""


def load_config(path: str) -> dict[str, str]:
    p = Path(path)
    if not p.exists():
        raise ConfigError(f"config file not found: {p}")
    result: dict[str, str] = {}
    try:
        for i, line in enumerate(p.read_text(encoding="utf-8").splitlines(), start=1):
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                raise ConfigError(f"invalid line {i}: {line!r}")
            key, value = line.split("=", 1)
            key = key.strip(); value = value.strip()
            if not key:
                raise ConfigError(f"empty key at line {i}")
            if key in result:
                raise ConfigError(f"duplicate key at line {i}: {key}")
            result[key] = value
        return result
    except OSError as e:
        # I/O issues; chain for context
        raise ConfigError(f"failed reading config: {e}") from e

Antwoord 6 — Re‑raise met logging

import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)


def risky_div(a: float, b: float) -> float:
    return a / b

if __name__ == "__main__":
    try:
        print(risky_div(1, 0))
    except Exception:
        log.exception("Risky division failed")  # logs traceback
        raise  # re-raise so callers still see the error

Antwoord 7 — Exception chaining

def parse_price(text: str) -> int:
    try:
        return int(text)
    except ValueError as e:
        raise ValueError(f"invalid price '{text}'") from e

if __name__ == "__main__":
    print(parse_price("12"))
    print(parse_price("abc"))  # will show chained traceback

Antwoord 8 — Recoverable vs. terminal met teller

import sys

errors = 0
while True:
    raw = input("Amount (empty to stop): ").strip()
    if raw == "":
        break
    try:
        amount = float(raw.replace(",", "."))
        print(f"OK: {amount:.2f}")
        errors = 0  # reset on success
    except ValueError:
        errors += 1
        print("Invalid amount, try again (digits, comma or dot).")
        if errors >= 3:
            print("Too many errors in a row — aborting.")
            sys.exit(1)

Antwoord 9 — Te brede excepties aanscherpen

# BAD: catches everything, hides the real cause, and the try-block is too large
# try:
#     amount = float(input("Amount: "))
#     print(100 / amount)
# except Exception:
#     print("Something went wrong")

# GOOD: small risky parts and specific exceptions
raw = input("Amount: ").strip()
try:
    amount = float(raw)
except ValueError:
    print("Please enter a valid number.")
else:
    try:
        print(100 / amount)
    except ZeroDivisionError:
        print("Division by zero is not allowed.")
  • Specifieke excepties geven betere feedback en je overschaduwt geen onverwachte fouten.
  • Kleine try‑scopes maken het duidelijker welke regel kon falen.

Antwoord 10 — Logging i.p.v. print

import logging

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger("app")

try:
    with open("missing.txt", "r", encoding="utf-8") as f:
        print(f.read())
except FileNotFoundError:
    log.error("File not found: missing.txt")

# logging.exception example (auto-includes traceback)
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Computation failed")

Uitdaging (optioneel) — Typed config (schets)

class ConfigError(Exception):
    pass

TRUE_SET = {"true", "yes", "1"}
FALSE_SET = {"false", "no", "0"}

def _coerce(value: str):
    v = value.strip()
    low = v.lower()
    if low in TRUE_SET:
        return True
    if low in FALSE_SET:
        return False
    try:
        if "." in v:
            return float(v)
        return int(v)
    except ValueError:
        return v  # leave as string

from pathlib import Path

def load_typed_config(path: str) -> dict[str, object]:
    p = Path(path)
    if not p.exists():
        raise ConfigError(f"config file not found: {p}")
    result: dict[str, object] = {}
    for i, line in enumerate(p.read_text(encoding="utf-8").splitlines(), start=1):
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        if "=" not in line:
            raise ConfigError(f"invalid line {i}: {line!r}")
        key, value = line.split("=", 1)
        key = key.strip(); value = value.strip()
        if not key:
            raise ConfigError(f"empty key at line {i}")
        if key in result:
            raise ConfigError(f"duplicate key at line {i}: {key}")
        result[key] = _coerce(value)
    return result

Antwoorden — Hoofdstuk 8: Bestanden lezen en schrijven

Opmerking: Code is in het Engels, met korte Nederlandse toelichting erboven of eronder.

Antwoord 1 — Merge .txt bestanden

from pathlib import Path


def merge_txt(dir_path: str | Path, out_name: str = "all.txt") -> None:
    d = Path(dir_path)
    out = d / out_name
    parts: list[str] = []
    for p in sorted(d.glob("*.txt")):
        if p.name == out_name:
            continue  # skip the output file itself if present
        text = p.read_text(encoding="utf-8", errors="replace")
        parts.append(text.rstrip("\n"))
    out.write_text("\n\n".join(parts) + ("\n" if parts else ""), encoding="utf-8")


if __name__ == "__main__":
    merge_txt("")

Antwoord 2 — tail(path, n)

from pathlib import Path
from collections import deque

def tail(path: str | Path, n: int) -> list[str]:
    # Simple approach: read all lines and slice. For medium files this is ok.
    lines = Path(path).read_text(encoding="utf-8", errors="replace").splitlines()
    return lines[-n:] if n > 0 else []

if __name__ == "__main__":
    print(tail("poem.txt", 5))

Antwoord 3 — copy_file (binair)

from pathlib import Path

def copy_file(src: str | Path, dst: str | Path, chunk_size: int = 8192) -> None:
    src_p = Path(src)
    dst_p = Path(dst)
    with src_p.open("rb") as f_in, dst_p.open("wb") as f_out:
        while True:
            chunk = f_in.read(chunk_size)
            if not chunk:
                break
            f_out.write(chunk)

if __name__ == "__main__":
    copy_file("image.jpg", "copy.jpg")

Antwoord 4 — grep‑achtig filter (case‑insensitive)

from pathlib import Path

def grep_ci(path: str | Path, needle: str) -> None:
    n = needle.lower()
    with Path(path).open("r", encoding="utf-8", errors="replace") as f:
        for line in f:
            if n in line.lower():
                print(line.rstrip())

if __name__ == "__main__":
    grep_ci("app.log", "error")

Antwoord 5 — CSV lezen (naïef) en net printen

from pathlib import Path

def print_items_table(csv_path: str | Path) -> None:
    rows: list[tuple[str, int, float]] = []
    with Path(csv_path).open("r", encoding="utf-8", newline="") as f:
        for line in f:
            name, qty, price = line.rstrip("\n").split(",")
            rows.append((name.strip(), int(qty), float(price)))
    total = 0.0
    print(f"{'Item':<12}{'Qty':>5}{'Price':>10}{'Total':>10}")
    for name, qty, price in rows:
        line_total = qty * price
        total += line_total
        print(f"{name:<12}{qty:>5}{price:>10.2f}{line_total:>10.2f}")
    print(f"{'Grand total:':<27}{total:>10.2f}")

if __name__ == "__main__":
    print_items_table("items.csv")

Antwoord 6 — JSON schrijven en lezen

from pathlib import Path
import json

def write_config(path: Path, data: dict) -> None:
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

def read_config(path: Path) -> dict:
    text = path.read_text(encoding="utf-8")
    return json.loads(text)

if __name__ == "__main__":
    cfg_path = Path("config.json")
    data = {"name": "Ava", "age": 30}
    try:
        write_config(cfg_path, data)
        loaded = read_config(cfg_path)
        print(loaded.get("name"))
    except FileNotFoundError:
        print("Config file not found")
    except json.JSONDecodeError as e:
        print("Invalid JSON:", e)

Antwoord 7 — Atomisch schrijven

from pathlib import Path
import os

def atomic_write_text(path: Path, text: str) -> None:
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(text, encoding="utf-8")
    os.replace(tmp, path)

if __name__ == "__main__":
    atomic_write_text(Path("data.txt"), "new content\n")

Antwoord 8 — Newline normalisatie

from pathlib import Path

def write_unix_newlines(path: Path, lines: list[str]) -> None:
    with path.open("w", encoding="utf-8", newline="\n") as f:
        f.writelines([line.rstrip("\n") + "\n" for line in lines])

if __name__ == "__main__":
    write_unix_newlines(Path("out.txt"), ["A", "B", "C"]) 

Antwoord 9 — Safe read_text

from pathlib import Path
from typing import Optional

def safe_read_text(path: Path) -> Optional[str]:
    try:
        return path.read_text(encoding="utf-8")
    except FileNotFoundError:
        print("File not found:", path)
        return None
    except PermissionError:
        print("No permission to read:", path)
        return None

if __name__ == "__main__":
    print(safe_read_text(Path("maybe.txt")))

Antwoord 10 — Directory listing met groottes

from pathlib import Path

def list_dir(path: str | Path) -> None:
    p = Path(path)
    files = 0
    total_bytes = 0
    for entry in p.iterdir():
        if entry.is_dir():
            kind = "DIR"
            size_str = ""
        else:
            kind = "FILE"
            size = entry.stat().st_size
            total_bytes += size
            files += 1
            size_str = f" {size:>8d} B"
        print(f"{kind:4} {entry.name}{size_str}")
    print(f"-- Files: {files}, Total bytes: {total_bytes}")

if __name__ == "__main__":
    list_dir(".")

Uitdaging (optioneel) — Mini To‑Do CLI met bestand

from pathlib import Path
import sys

TODO = Path("todo.txt")


def add_item(text: str) -> None:
    with TODO.open("a", encoding="utf-8") as f:
        f.write(text.strip() + "\n")


def list_items() -> None:
    if not TODO.exists():
        print("No items yet")
        return
    with TODO.open("r", encoding="utf-8") as f:
        for i, line in enumerate(f, start=1):
            print(f"{i}. {line.rstrip()}")


def done(n: int) -> None:
    # choice: remove the line permanently
    if not TODO.exists():
        return
    lines = TODO.read_text(encoding="utf-8").splitlines()
    if 1 <= n <= len(lines):
        del lines[n - 1]
        TODO.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: todo.py [add <text>|list|done <n>]")
        raise SystemExit(1)
    cmd = sys.argv[1]
    if cmd == "add" and len(sys.argv) >= 3:
        add_item(" ".join(sys.argv[2:]))
    elif cmd == "list":
        list_items()
    elif cmd == "done" and len(sys.argv) >= 3:
        try:
            n = int(sys.argv[2])
        except ValueError:
            print("Please provide an integer index for 'done'.")
            raise SystemExit(1)
        done(n)
    else:
        print("Unknown command or missing arguments.")
        raise SystemExit(1)

Antwoorden — Hoofdstuk 9: Modules en virtuele omgevingen

Opmerking: Code is in het Engels, met korte Nederlandse toelichtingen in comments.

Antwoord 1 — Eigen module importeren (temperature)

# temperature.py

def c_to_f(c: float) -> float:
    return (c * 9/5) + 32

def f_to_c(f: float) -> float:
    return (f - 32) * 5/9
# app.py
from temperature import c_to_f, f_to_c

print(c_to_f(20.0))  # 68.0
print(f_to_c(32.0))  # 0.0

Antwoord 2 — import vs from-import

# variant A
import math
print(math.pi)

# variant B
from math import pi
print(pi)
# Comment: with 'import math' you use the qualified name math.pi (clearer where it comes from).
# With 'from math import pi' the name 'pi' is imported directly into your module namespace.

Antwoord 3 — name == "main"

# temperature.py (extended)
import sys

def c_to_f(c: float) -> float:
    return (c * 9/5) + 32

def f_to_c(f: float) -> float:
    return (f - 32) * 5/9

if __name__ == "__main__":
    c = float(sys.argv[1]) if len(sys.argv) > 1 else 0.0
    print(c_to_f(c))

Antwoord 4 — Het importpad bekijken

# show_path.py
import sys
for p in sys.path:
    print(p)
# Comment (NL): De eerste entry is vaak de directory van het actieve script of '',
# wat de current working directory representeert. Startpositie kan verschillen per aanroeplocatie.

Antwoord 5 — Eenvoudig package calc/

# calc/__init__.py
__all__ = ["add", "sub", "mul", "div"]
from .ops import add, sub, mul, div
# calc/ops.py

def add(a, b):
    return a + b

def sub(a, b):
    return a - b

def mul(a, b):
    return a * b

def div(a, b):
    if b == 0:
        raise ZeroDivisionError("division by zero")
    return a / b
# main.py
from calc.ops import add, div

print(add(2, 3))
print(div(10, 2))

Antwoord 6 — Relatieve import binnen package

# calc/cli.py
from .ops import add

def main() -> None:
    import sys
    if len(sys.argv) >= 3:
        a = float(sys.argv[1]); b = float(sys.argv[2])
    else:
        a, b = 2.0, 3.0
    print(add(a, b))

if __name__ == "__main__":
    main()
# Run as module: python -m calc.cli 4 5

Antwoord 7 — Vermijd star-import

# BAD
# from math import *

# GOOD (explicit)
from math import sqrt, pi
print(sqrt(9), pi)

# or
import math
print(math.sqrt(9), math.pi)
# Comment: '*' vervuilt je namespace en kan namen overschrijven, wat debugging lastiger maakt.

Antwoord 8 — Stdlib mini-taken

import random
from datetime import datetime
from pathlib import Path

print(random.choice(["red", "green", "blue"]))
print(datetime.now().isoformat(timespec="seconds"))
print("notes.txt exists?", (Path("notes.txt").exists()))

Antwoord 9 — pip/venv (conceptueel) en requests

# Commands (run in a shell):
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell
# .venv\Scripts\Activate.ps1
python -m pip install requests
python -m pip freeze > requirements.txt
# fetch.py (run inside an activated venv)
import requests

resp = requests.get("https://httpbin.org/get", timeout=10)
print(resp.status_code)
print(resp.headers.get("Content-Type"))

Antwoord 10 — Naamconflict met stdlib

# random.py (your file) -- DON'T DO THIS NAME

def foo():
    return "bar"
# other_script.py
import random
print(random.choice([1, 2, 3]))  # This will fail or import your local random.py instead of stdlib
# Fix: rename your file to myrandom.py and try again
import random
print(random.choice([1, 2, 3]))  # works, using stdlib random
# Comment (NL): De bestandsnaam 'random.py' overschaduwde de standaardbibliotheekmodule met dezelfde naam.
# Door te hernoemen valt de import weer naar de stdlib.

Uitdaging — greet package met CLI

# greet/__init__.py
__all__ = ["hello"]

def hello(name: str) -> str:
    return f"Hello, {name}!"
# greet/cli.py
from . import hello

def main() -> None:
    import sys
    name = sys.argv[1] if len(sys.argv) > 1 else "world"
    print(hello(name))

if __name__ == "__main__":
    main()
# Usage: python -m greet.cli Ava  (ensure PYTHONPATH includes parent dir or install in venv)

Basis testen en debuggen

Antwoorden — Hoofdstuk 12: Eindproject (projectuitwerkingen)

Opmerking:

  • Uitleg in het Nederlands; alle code is in het Engels, conform het boek.
  • Per projectoptie vind je een referentie‑implementatie die aan de eisen voldoet:
    • Minstens 2 modules en 5 functies.
    • Persistente opslag waar gevraagd (highscore/todo/questions).
    • Basistests: minimaal 5 asserts of 2 unittest‑cases.
    • Een korte README‑schets voor studenten.
  • Je mag de structuur aanpassen; dit zijn compacte, didactische voorbeelden.

Inhoud

  • Project A: Number Guessing Game (met highscore‑bestand)
  • Project B: To‑Do CLI (persistente opslag + eenvoudige tests)
  • Project C: Tekst‑gebaseerde quiz (vragen uit JSON)

Project A — Number Guessing Game (highscore)

Structuur (voorbeeld):

  • guessgame/
    • init.py
    • game.py (spelloop, input/feedback)
    • storage.py (highscore lezen/schrijven)
    • cli.py (entry point)
    • tests_basic.py (eenvoudige asserts/unittests)

Implementatie (kerncode):

# guessgame/storage.py
from pathlib import Path
from typing import Optional

HIGHSCORE = Path("highscore.txt")

def read_highscore() -> Optional[int]:
    if not HIGHSCORE.exists():
        return None
    try:
        txt = HIGHSCORE.read_text(encoding="utf-8").strip()
        return int(txt) if txt else None
    except ValueError:
        return None

def write_highscore(score: int) -> None:
    # lower is better (attempts)
    prev = read_highscore()
    if prev is None or score < prev:
        HIGHSCORE.write_text(str(score), encoding="utf-8")
# guessgame/game.py
import random
from typing import Callable, Optional
from .storage import read_highscore, write_highscore

Feedback = str

def pick_secret(low: int = 1, high: int = 100, rng: Optional[random.Random] = None) -> int:
    r = rng or random
    return r.randint(low, high)

def feedback(secret: int, guess: int) -> Feedback:
    if guess < secret:
        return "Higher"
    if guess > secret:
        return "Lower"
    return "Correct"

def play_round(secret: int, reader: Callable[[str], str], writer: Callable[[str], None]) -> int:
    attempts = 0
    while True:
        raw = reader("Your guess: ").strip()
        if not raw:
            writer("Bye!")
            break
        try:
            g = int(raw)
        except ValueError:
            writer("Enter an integer.")
            continue
        attempts += 1
        fb = feedback(secret, g)
        writer(fb)
        if fb == "Correct":
            break
    return attempts

def maybe_update_highscore(attempts: int) -> None:
    write_highscore(attempts)
# guessgame/cli.py
from .game import pick_secret, play_round, maybe_update_highscore
from .storage import read_highscore

def main() -> None:
    import sys
    low, high = 1, 100
    if len(sys.argv) == 3:
        low = int(sys.argv[1]); high = int(sys.argv[2])
    secret = pick_secret(low, high)
    print(f"Guess the number between {low} and {high} (empty to quit)")
    attempts = play_round(secret, input, print)
    if attempts > 0:
        print(f"You used {attempts} attempts.")
        maybe_update_highscore(attempts)
        hs = read_highscore()
        if hs is not None:
            print(f"Best so far: {hs} attempts")

if __name__ == "__main__":
    main()

Basistests (mix van asserts en kleine unittests mogelijk):

# guessgame/tests_basic.py
from guessgame.game import feedback, play_round

# pure function asserts
assert feedback(10, 5) == "Higher"
assert feedback(10, 15) == "Lower"
assert feedback(10, 10) == "Correct"

# simulate I/O for play_round
class IO:
    def __init__(self, inputs):
        self.inputs = list(inputs)
        self.out = []
    def reader(self, prompt: str) -> str:
        return self.inputs.pop(0) if self.inputs else ""
    def writer(self, msg: str) -> None:
        self.out.append(msg)

io = IO(["7", "10"])  # suppose secret=10
attempts = play_round(10, io.reader, io.writer)
assert attempts == 2
assert io.out[-1] == "Correct"

README‑schets (voor studenten, in English code allowed, tekst NL):

# Number Guessing Game

- Run: `python -m guessgame.cli [low high]`
- Goal: Guess the secret number; program gives "Higher"/"Lower" feedback.
- Highscore: best (fewest attempts) stored in `highscore.txt`.
- Modules: `game.py` (logic), `storage.py` (file I/O), `cli.py` (entry point).
- Tests: see `tests_basic.py` (asserts on pure functions + simulated I/O).

Project B — To‑Do CLI (persistente opslag)

Structuur (voorbeeld):

  • todo/
    • init.py
    • store.py (bestands‑I/O)
    • core.py (bewerkingen op items)
    • cli.py (commando’s: add/list/done)
    • tests_basic.py

Implementatie (kerncode):

# todo/store.py
from pathlib import Path
from typing import Iterable

TODO = Path("todo.txt")

def read_items() -> list[str]:
    if not TODO.exists():
        return []
    return [line.rstrip("\n") for line in TODO.read_text(encoding="utf-8").splitlines()]

def write_items(items: Iterable[str]) -> None:
    text = "\n".join(items) + ("\n" if items else "")
    TODO.write_text(text, encoding="utf-8")
# todo/core.py
from typing import Iterable

def add_item(items: list[str], text: str) -> list[str]:
    text = text.strip()
    return items + [text] if text else items

def list_lines(items: Iterable[str]) -> list[str]:
    return [f"{i}. {it}" for i, it in enumerate(items, start=1)]

def done_item(items: list[str], index: int) -> list[str]:
    if 1 <= index <= len(items):
        new = list(items)
        del new[index - 1]
        return new
    return items
# todo/cli.py
from .store import read_items, write_items
from .core import add_item, list_lines, done_item

def main() -> None:
    import sys
    args = sys.argv[1:]
    if not args:
        print("Usage: todo.py [add <text>|list|done <n>]")
        raise SystemExit(1)
    cmd = args[0]
    items = read_items()
    if cmd == "add" and len(args) >= 2:
        items = add_item(items, " ".join(args[1:]))
        write_items(items)
    elif cmd == "list":
        for line in list_lines(items):
            print(line)
    elif cmd == "done" and len(args) >= 2:
        try:
            n = int(args[1])
        except ValueError:
            print("Please provide an integer index.")
            raise SystemExit(1)
        items = done_item(items, n)
        write_items(items)
    else:
        print("Unknown command or missing arguments.")
        raise SystemExit(1)

if __name__ == "__main__":
    main()

Basistests:

# todo/tests_basic.py
from todo.core import add_item, list_lines, done_item

items: list[str] = []
items = add_item(items, "buy milk")
items = add_item(items, "send email")
assert len(items) == 2
assert list_lines(items)[0].startswith("1.")
items2 = done_item(items, 1)
assert len(items2) == 1 and items2[0] == "send email"
# invalid index should keep list unchanged
assert done_item(items, 99) == items

README‑schets:

# To‑Do CLI

Commands:
- `add <text>`: append a new item.
- `list`: show numbered items.
- `done <n>`: remove item n (1‑based).

Modules: `store.py` (file I/O), `core.py` (logic), `cli.py` (entry).
Tests: see `tests_basic.py` for simple asserts over pure functions.

Project C — Tekst‑gebaseerde quiz (JSON)

Structuur (voorbeeld):

  • quiz/
    • init.py
    • io.py (laden vragenbestand)
    • engine.py (afspelen quiz, scoring)
    • cli.py (entry)
    • questions.json (voorbeeldbestand)
    • tests_basic.py

Voorbeeld questions.json:

[
  {"q": "Capital of France?", "a": "paris"},
  {"q": "2+2?", "a": "4"}
]

Implementatie (kerncode):

# quiz/io.py
import json
from pathlib import Path
from typing import Iterable

def load_questions(path: str | Path) -> list[dict]:
    p = Path(path)
    data = json.loads(p.read_text(encoding="utf-8"))
    if not isinstance(data, list):
        raise ValueError("questions file must be a JSON list")
    return data
# quiz/engine.py
from typing import Callable, Iterable

def ask_one(q: str, answer: str, reader: Callable[[str], str], writer: Callable[[str], None]) -> bool:
    user = reader(q + " ").strip().lower()
    return user == answer.lower()

def run_quiz(qa: Iterable[dict], reader, writer) -> int:
    correct = 0
    for item in qa:
        if ask_one(item["q"], item["a"], reader, writer):
            writer("Correct")
            correct += 1
        else:
            writer("Wrong")
    return correct
# quiz/cli.py
from .io import load_questions
from .engine import run_quiz

def main() -> None:
    import sys
    path = sys.argv[1] if len(sys.argv) >= 2 else "questions.json"
    qa = load_questions(path)
    print("Answer the questions (empty line to skip)")
    def reader(prompt: str) -> str:
        try:
            return input(prompt)
        except EOFError:
            return ""
    score = run_quiz(qa, reader, print)
    print(f"Score: {score}/{len(qa)}")

if __name__ == "__main__":
    main()

Basistests:

# quiz/tests_basic.py
from quiz.engine import ask_one, run_quiz

assert ask_one("2+2?", "4", lambda _: "4", lambda _m: None) is True
assert ask_one("2+2?", "4", lambda _: "5", lambda _m: None) is False

class IO:
    def __init__(self, answers):
        self.answers = list(answers)
        self.out = []
    def reader(self, _prompt: str) -> str:
        return self.answers.pop(0) if self.answers else ""
    def writer(self, msg: str) -> None:
        self.out.append(msg)

qa = [{"q": "Capital?", "a": "Paris"}, {"q": "2+2?", "a": "4"}]
io = IO(["paris", "4"])  # all correct
score = run_quiz(qa, io.reader, io.writer)
assert score == 2
assert io.out.count("Correct") == 2

README‑schets:

# Quiz (text‑based)

- Provide a JSON file with questions: objects having keys `q` and `a`.
- Run: `python -m quiz.cli questions.json`
- Modules: `io.py` (load), `engine.py` (run/scoring), `cli.py` (entry).
- Tests: see `tests_basic.py` (pure logic tested via simulated I/O).

Tips voor beoordeling (docent):

  • Controleer of studenten minimaal 2 modules en 5 functies inzetten (scheiding I/O vs. logica).
  • Laat hen een korte README in het project toevoegen met gebruiksvoorbeelden.
  • Tests: stimuleer pure functies zodat asserts/unittests eenvoudig zijn.
  • Extra punten voor nette foutafhandeling (bestanden/JSON), type hints, en duidelijke naamgeving.