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 function | Small functions |
|---|---|
| Reads input, calculates, prints | Each step is its own function |
| To test the discount, you must type a whole order | Call discount(200, 10) and check it |
| A change in GST means hunting through 60 lines | Change one line inside gst() |
| Hard to reuse in another screen | Reuse 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.