Skip to content
aviral gupta

// B3.4 · ~30 min · Beginner

Lambdas, docstrings, annotations and PEP 8

After this lesson you can sort with a lambda as the key, document a function with a proper docstring and annotations, and name and lay out code the way PEP 8 asks.

Lesson 4 of 5 in B3 Functions

You will be able to

  • Write a lambda and pass it as the key of sorted()
  • Write docstrings that follow the conventions, and add and read annotations
  • Apply PEP 8: names in lowercase_with_underscores, 4-space indents, 79-character lines, spacing
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B3.3: what does this print?

    def f(*args, **kwargs):
        print(len(args), len(kwargs))
    
    f(1, 2, x=3)
  2. Predict · Activity 2 of 7

    Predict before you read on: the key is a lambda that takes the last letter. What does this print?

    words = ["kiwi", "fig", "banana", "apple"]
    print(sorted(words, key=lambda w: w[-1]))
  3. Practice · Activity 3 of 7

    Fill in the keyword so that the pairs are sorted by score, lowest first.

    scores = [["ann", 82], ["bo", 95], ["cy", 78]]
    print(sorted(scores, key=____ pair: pair[1]))
    key= pair: pair[1]
  4. Practice · Activity 4 of 7

    What does this print?

    def greet(name: str, times: int = 1) -> str:
        return name * times
    
    print(greet.__annotations__)
  5. Practice · Activity 5 of 7

    Match each part of a program to the PEP 8 rule for it.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. The annotation says int, but a string is passed. What does this print?

    def double(n: int) -> int:
        return n * 2
    
    print(double("ab"))
  7. Apply · Activity 7 of 7

    Mini-task. Write longest_first(words: list[str]) -> list[str] that returns the words sorted from longest to shortest, using sorted with a key and reverse=True. Give it a one-line docstring. Print the result for ["fig", "banana", "kiwi", "plum"], and then its __doc__.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Sorting a team list

Plain sorted puts capital letters before small ones, so "Bo" comes before "ada". sort_names fixes that with a lambda key that compares lowercase names; name.lower() gives the name in small letters. With by_length=True it sorts again by length, and the stable sort keeps names of equal length in alphabetical order. The function is annotated and has a multi-line docstring.

main.py

def sort_names(names: list[str], *, by_length: bool = False) -> list[str]:
    """Return the names sorted alphabetically, ignoring case.

    With by_length=True, shorter names come first; names of the
    same length keep their alphabetical order.
    """
    ordered = sorted(names, key=lambda name: name.lower())
    if by_length:
        ordered = sorted(ordered, key=len)
    return ordered


team = ["maya", "Bo", "leon", "ada", "Christopher"]
print(sorted(team))
print(sort_names(team))
print(sort_names(team, by_length=True))
print(sort_names.__annotations__)
print(sort_names.__doc__)

Run it with

python main.py

Output

['Bo', 'Christopher', 'ada', 'leon', 'maya']
['ada', 'Bo', 'Christopher', 'leon', 'maya']
['Bo', 'ada', 'leon', 'maya', 'Christopher']
{'names': list[str], 'by_length': <class 'bool'>, 'return': list[str]}
Return the names sorted alphabetically, ignoring case.

With by_length=True, shorter names come first; names of the
same length keep their alphabetical order.
  • Without a key, every capital letter sorts before every small letter.
  • key=len needs no lambda: any function of one argument can be the key.
  • leon and maya have the same length and stay in alphabetical order, because the sort is stable.
  • The docstring keeps its blank second line; Python removes the indentation of the later lines.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Sorting with keys

Complete two functions. sort_by_last_letter(words) returns the words sorted by their last letter, with a lambda as the key. sort_by_length(words, *, longest_first=False) returns them sorted by length, shortest first, or longest first when longest_first=True. Words of equal length keep their order. Both return new lists and leave the argument unchanged.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    sorted(words, key=…) returns a new sorted list and leaves words alone.

  2. Hint 2

    For the last letter, the key is lambda word: word[-1]. For the length, key=len is enough.

  3. Hint 3

    Pass the flag straight on: sorted(words, key=len, reverse=longest_first).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

def sort_by_last_letter(words: list[str]) -> list[str]:
    """Return the words sorted by their last letter."""
    return sorted(words, key=lambda word: word[-1])


def sort_by_length(words: list[str], *, longest_first: bool = False) -> list[str]:
    """Return the words sorted by length, shortest first by default."""
    return sorted(words, key=len, reverse=longest_first)


print(sort_by_last_letter(["kiwi", "fig", "banana", "apple"]))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

def sort_by_last_letter(words: list[str]) -> list[str]:
    """Return the words sorted by their last letter."""
    return list(words)  # sort them instead


def sort_by_length(words: list[str], *, longest_first: bool = False) -> list[str]:
    """Return the words sorted by length, shortest first by default."""
    return list(words)  # sort them instead


print(sort_by_last_letter(["kiwi", "fig", "banana", "apple"]))

test_main.py

from main import sort_by_last_letter, sort_by_length


def test_last_letter():
    """Sorted by the last letter"""
    got = sort_by_last_letter(["kiwi", "fig", "banana", "apple"])
    assert got == ["banana", "apple", "fig", "kiwi"], f"sort_by_last_letter(['kiwi', 'fig', 'banana', 'apple']) returned {got!r}"


def test_shortest_first():
    """sort_by_length puts the shortest word first"""
    got = sort_by_length(["pear", "fig", "banana"])
    assert got == ["fig", "pear", "banana"], f"sort_by_length(['pear', 'fig', 'banana']) returned {got!r}, expected ['fig', 'pear', 'banana']"


def test_longest_first():
    """longest_first=True reverses the order"""
    got = sort_by_length(["pear", "fig", "banana"], longest_first=True)
    assert got == ["banana", "pear", "fig"], f"sort_by_length(['pear', 'fig', 'banana'], longest_first=True) returned {got!r}"


def test_equal_lengths_keep_order():
    """Words of equal length keep their order"""
    got = sort_by_length(["bb", "aa", "c"])
    assert got == ["c", "bb", "aa"], f"sort_by_length(['bb', 'aa', 'c']) returned {got!r}, expected ['c', 'bb', 'aa']"


def test_argument_unchanged():
    """The list passed in is not changed"""
    words = ["kiwi", "fig"]
    sort_by_length(words)
    assert words == ["kiwi", "fig"], f"after sort_by_length(words), words is {words!r}; use sorted(), which returns a new list"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Tidy up a function

calc_total works, but breaks PEP 8 and has no documentation. Rename its parameters to prices and tax_rate, indent with 4 spaces, give each statement its own line, and add spaces around operators. Add a one-line docstring and annotations: prices is list[float], tax_rate is float with default 0.19, and it returns a float. The result must stay the same.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Start with the def line: def calc_total(prices: list[float], tax_rate: float = 0.19) -> float:. PEP 8 wants spaces around = here, because the parameter is annotated.

  2. Hint 2

    Split for p in Prices:t=t+p into a for line and an indented body line, and use clear names: total and price.

  3. Hint 3

    The docstring goes directly under the def line, indented by 4 spaces, for example """Return the sum of the prices plus tax."""

Show a solution

One way to solve it. Yours can look different and still pass the checks.

def calc_total(prices: list[float], tax_rate: float = 0.19) -> float:
    """Return the sum of the prices plus tax."""
    total = 0.0
    for price in prices:
        total = total + price
    return total + total * tax_rate


print(calc_total([10, 20]))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

def calc_total(Prices,Tax_Rate=0.19):
  t=0
  for p in Prices:t=t+p
  return t+t*Tax_Rate


print(calc_total([10, 20]))

test_main.py

from main import calc_total


def test_result_unchanged():
    """calc_total([10, 20]) is still 35.7"""
    got = calc_total([10, 20])
    assert round(got, 2) == 35.7, f"calc_total([10, 20]) returned {got!r}, expected 35.7"


def test_tax_rate_keyword():
    """The parameter is called tax_rate"""
    got = calc_total([10.0], tax_rate=0.0)
    assert got == 10.0, f"calc_total([10.0], tax_rate=0.0) returned {got!r}, expected 10.0"


def test_docstring():
    """It has a docstring: capital letter first, period last"""
    doc = calc_total.__doc__
    assert doc, "calc_total has no docstring"
    first = doc.strip().splitlines()[0]
    assert first[0].isupper() and first.endswith("."), f"the docstring's first line is {first!r}"


def test_annotations():
    """prices, tax_rate and the return value are annotated"""
    names = sorted(calc_total.__annotations__)
    assert names == ["prices", "return", "tax_rate"], f"calc_total.__annotations__ has the keys {names!r}"
    assert calc_total.__annotations__["return"] is float, "the return annotation should be float"


def test_four_space_indent():
    """Every indented line uses a multiple of 4 spaces"""
    with open("main.py", encoding="utf-8") as f:
        lines = f.read().splitlines()
    bad = [line for line in lines if line.startswith(" ") and (len(line) - len(line.lstrip(" "))) % 4 != 0]
    assert not bad, f"these lines are not indented by a multiple of 4 spaces: {bad!r}"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Writing return inside a lambda

words = ["kiwi", "fig", "banana"]
print(sorted(words, key=lambda w: return len(w)))

What Python prints

SyntaxError: invalid syntax

Why, and the fix

A lambda body is a single expression, and its value is returned automatically. return is a statement, so it is not allowed there. Write lambda w: len(w), or simply key=len.

Calling the key function instead of passing it

words = ["kiwi", "fig", "banana"]
print(sorted(words, key=len()))

What Python prints

TypeError: len() takes exactly one argument (0 given)

Why, and the fix

key needs the function itself, which sorted calls once per item. len() calls len right away, with no argument. Pass the name without parentheses: key=len.

Misspelling __annotations__

def greet(name: str) -> str:
    return "Hello, " + name

print(greet.__annotation__)

What Python prints

AttributeError: 'function' object has no attribute '__annotation__'

Why, and the fix

The attribute is __annotations__, plural, with two underscores on each side. Python 3.14 adds a suggestion to the message: Did you mean: '__annotations__'?

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

lambda makes a small function in place

lambda word: len(word) is a function with one parameter, word, that returns len(word). The body is a single expression: no return, no statements. Its main use is to pass a small function to another one. sorted(words, key=lambda word: word[-1]) calls the key once per item and sorts by the results, here the last letters. reverse=True flips the order. The sort is stable: items with equal keys keep their order. PEP 8 says: if you would give a lambda a name, write a def instead.

Docstrings and annotations

A docstring's first line is a short summary that starts with a capital letter and ends with a period. A longer docstring continues after one blank line. Annotations document types: def greet(name: str, times: int = 1) -> str:. They are stored in greet.__annotations__, but Python does not check them when the program runs: greet(5) still runs until something fails. Tools such as mypy read them and report mistakes before running; the checks on this site run mypy on every solution.

PEP 8 in five rules

PEP 8 is the style guide most Python projects follow. Indent with 4 spaces, never tabs. Keep lines at most 79 characters. Name functions and variables in lowercase_with_underscores, classes in CapWords such as ShoppingCart, and constants in UPPER_CASE. Put two blank lines around top-level functions. Put spaces around operators and after commas, but not inside brackets: a = f(1, 2) + g(3, 4). A keyword default has no spaces around =, unless the parameter is annotated: times: int = 1.

Sources

Last reviewed September 29, 2026