Good functions: one job, docs and tests

Part of the free LogicWiz Python course, Module 3: Functions. The examples and practice run in the app.

Meher's 60-line function worked out the bill, the discount and printed the receipt, all at once. A discount bug took her an hour to find. Split into four small tested functions, the same bug showed up in one minute.

You'll learn to

  • Give each function one clear job and a verb-like name
  • Write a docstring and read it back
  • Test a function with `assert` and good messages
  • Build a bigger result by combining small functions

Learn

One job per function

A good function does one thing you can say in a short sentence: "adds GST", "checks if a year is a leap year". If you need the word and to describe it, split it.

Helpers should return values, not print them. Then the caller decides what to do: print, save, or use in more maths. Keep print and input at the edges of the program.

Messy vs clean

One big functionSmall functions
Reads input, calculates, printsEach step is its own function
To test the discount, you must type a whole orderCall discount(200, 10) and check it
A change in GST means hunting through 60 linesChange one line inside gst()
Hard to reuse in another screenReuse gst() anywhere

A docstring says what it does

def discount(amount, percent=0):
    """Return amount after a percent discount, rounded to 2 decimals.

    discount(200, 10) -> 180.0
    """
    return round(amount * (100 - percent) / 100, 2)

print(discount.__doc__)
print(discount(200, 10))

A docstring is a string written right below the def line. Editors show it when you hover, help(discount) prints it, and it lives in discount.__doc__. A # comment explains how; a docstring says what and gives an example.

assert: a one-line test

assert condition, "message" does nothing when the condition is true. When it is false, the program stops with AssertionError and your message.

Write a few asserts under each function: one normal case, one edge case (0, an empty value, the exact boundary like 33 marks) and one unusual case. If you change the function later and break something, an assert shouts immediately.

Tests that pass are silent

def discount(amount, percent=0):
    return round(amount * (100 - percent) / 100, 2)

assert discount(200, 10) == 180.0, "10% off 200 should be 180"
assert discount(99) == 99, "no discount by default"
assert discount(0, 50) == 0, "zero stays zero"
print("all tests passed")

A failing test points at the bug

def gst(amount):
    return amount * 0.05   # bug: forgot to add the amount

assert gst(100) == 105, "gst(100) should be 105"
print("you won't see this")

AssertionError: gst(100) should be 105. A good message tells you exactly which case broke.

Small functions, combined

def subtotal(price, qty):
    return price * qty

def discount(amount, percent):
    return amount * (100 - percent) / 100

def gst(amount):
    return round(amount * 1.05, 2)

def final_bill(price, qty, percent=0):
    return gst(discount(subtotal(price, qty), percent))

print(final_bill(40, 5, 10))

Read final_bill from the inside out: 40 × 5 = 200, 10% off is 180.0, plus GST is 189.0. Each piece can be tested alone.

Compare floats after rounding. 0.1 + 0.2 == 0.3 is False, so a test like assert gst(10) == 10.5 only works because gst rounds. If a function returns raw floats, test with round(result, 2) == ….

Board and lab questions are almost always "Write a function …". Write a clear header with the exact name asked, return the answer (unless the question says display), and show one call. In a lab viva, being able to say how you tested it impresses the examiner.

In one line

One job per function, a docstring on top, and assert condition, "message" tests (no brackets) for normal, edge and boundary cases.