Why this matters
Writing a function that works is the first step. Writing one that others (including you, six months from now) can understand, trust and reuse is the real craft. This lesson covers how Python functions document themselves with docstrings, and the design habits that separate easy-to-maintain code from tangled code: pure functions, controlled side effects, one job per function, and keeping input/output at the edges. These habits also make testing (module 11) dramatically easier.
Docstrings
A docstring is a string literal that is the first statement in a function body:
def area(width, height):
"""Return the area of a width-by-height rectangle."""
return width * height
It is not a comment. Comments are discarded by the tokenizer, but a docstring is kept: the compiler stores it on the function object as __doc__, and tools read it from there.
print(area.__doc__)
help(area)
Return the area of a width-by-height rectangle.
Help on function area in module __main__:
area(width, height)
Return the area of a width-by-height rectangle.
help() (which the REPL uses, as do editors when you hover over a name) builds its page from the function's signature and docstring. Every built-in and standard library function is documented this way; help(str.split) is often faster than a web search.
Writing good docstrings
Conventions (from PEP 257, the docstring style guide):
- Use triple double quotes, even for one line.
- The first line is a short summary in the imperative mood, ending with a period: "Return ...", "Convert ...", not "This function returns ...".
- For more detail, leave a blank line, then describe the arguments, the return value, exceptions raised, and anything surprising.
def parse_duration(text):
"""Convert a duration like "1h30m" or "45m" into a number of minutes.
text: hours followed by "h" and/or minutes followed by "m".
Returns the total number of minutes as an int.
Raises ValueError if text is empty or malformed.
"""
...
A docstring says what the function does and how to call it, not how it is implemented. Implementation notes belong in comments next to the code they explain.
Pure functions and side effects
A pure function:
- returns a result that depends only on its arguments, and
- has no side effects: it does not print, read input, write files, change globals, or mutate its arguments.
def total_with_tax(prices, rate):
"""Return the sum of prices plus tax at the given rate."""
subtotal = 0
for p in prices:
subtotal += p
return subtotal * (1 + rate)
Call it with the same arguments a thousand times and you get the same answer and nothing else changes. That makes it trivial to test (assert total_with_tax([10, 20], 0.5) == 45.0), safe to reuse anywhere, and easy to reason about.
Side effects are not evil; a program with none would be useless, since printing and saving files are side effects. The point is to keep them few and visible. Compare:
discount = 0.1
def price_after_discount(price):
global discount
discount += 0.05 # surprise: every call changes the discount
return price * (1 - discount)
Here the same call returns different values each time, and the effect is invisible at the call site. Bugs like this are hard to find because the cause is far away from the symptom.
Mutating an argument is a side effect that is easy to do by accident; the next module shows exactly how, with lists. If a function mutates its argument on purpose, say so in its docstring and, by convention, return None (as list.sort() does), so that nobody mistakes it for a function that returns a new value.
One job per function
A function should do one thing, which you can name with a verb phrase. Signs a function does too much:
- its name needs "and":
read_and_validate_and_save; - it is longer than a screen;
- you cannot describe it in a one-line docstring summary;
- part of it would be useful elsewhere but cannot be reached.
Splitting is cheap in Python. Small, well-named functions read like prose:
def main():
text = read_input()
words = split_into_words(text)
counts = count_words(words)
print_report(counts)
Keep I/O at the edges
A powerful pattern: put all the reading and printing in a thin outer layer (often a function called main), and keep the logic in pure functions that receive data as arguments and return results.
stdin/files ---> main() ---> pure functions ---> main() ---> stdout/files
(I/O layer) (all the logic) (I/O layer)
Before:
def shipping():
weight = float(input())
if weight <= 1:
print("cost: 5")
else:
print("cost:", 5 + (weight - 1) * 2)
You cannot test the pricing rule without faking keyboard input and capturing the screen. After:
def shipping_cost(weight):
"""Return the shipping cost: 5 for up to 1 kg, plus 2 per extra kg."""
if weight <= 1:
return 5
return 5 + (weight - 1) * 2
def main():
weight = float(input())
print("cost:", shipping_cost(weight))
if __name__ == "__main__":
main()
Now shipping_cost(3) can be checked directly, reused by a web page or a batch job, and changed without touching any I/O. The last two lines mean "call main() only when this file is run as a program, not when it is imported". You will learn exactly why in module 10; learnprog's tests import your file, so this idiom is what lets a test call shipping_cost without main waiting for input.
Naming
Function names in Python use lower_case_with_underscores. Name functions for what they return or do: is_even(n) (returns a bool, reads as a question), word_count(text), send_email(...). Parameter names are part of your interface too: callers may use them as keywords, so def resize(image, width, height) is better than def resize(i, a, b).
Common mistakes
- A comment instead of a docstring.
# Return the areaabove or inside the function is not attached to it;help()will not show it. - Docstring not first. If any statement comes before the string, it is just an unused expression and
__doc__isNone. - Hidden side effects, such as a "get" function that also modifies a global or prints. Readers trust names;
get_priceshould not change anything. - Printing inside logic functions. It makes their output impossible to use programmatically and noisy to test. Return the data; let the caller decide how to show it.
- Over-splitting. A one-line function used once, with a name no clearer than its body, adds indirection without value. Split for clarity and reuse, not on principle.
Recap
- A docstring is a string literal as the first statement of a function; it becomes
__doc__and powershelp(). - Docstring summary line: imperative, one line, ends with a period; details after a blank line.
- Pure functions depend only on arguments and have no side effects: easy to test and reuse.
- Give each function one job, and keep input/output in a thin
mainlayer around pure logic.
Going further: skim PEP 257 ("Docstring Conventions") at peps.python.org/pep-0257, then run help(str.split) and help(round) and notice how the standard library structures its docstrings.