We’ve used type hints in every function since the first lesson of this section. This lesson covers the basics in one place, and introduces docstrings — the way to explain what a function does to anyone who reads it.

Type hints — a recap

A type hint tells a reader (and the type checker) what type a value should be:

def add(a: int, b: int) -> int:
    return a + b


name: str = "Manikandan"
ages: list[int] = [25, 30, 35]

Hints go after a colon for variables and parameters, and after -> for return types.

The basic types you’ll use

x: int = 0
x: float = 0.0
x: str = ""
x: bool = True

# collections
x: list[int] = [1, 2, 3]
x: tuple[int, str] = (1, "a")
x: set[str] = {"a", "b"}
x: dict[str, int] = {"a": 1, "b": 2}

# none
x: None = None

Modern Python (3.9+) lets you write list[int] directly. Older tutorials use List[int] imported from typing — you don’t need that anymore.

Union types — when a value can be one of several

Use the | operator to mean “either”:

def parse_int(text: str) -> int | None:
    if text.isdigit():
        return int(text)
    return None

int | None means “returns an int, or None if it fails”. This pattern is so common it has a special name — Optional — but X | None is the modern way to write it.

# all equivalent
x: int | None
x: str | int | float

Type hints for functions that take functions

When a parameter is a function, use Callable:

from collections.abc import Callable


def apply_twice(func: Callable[[int], int], x: int) -> int:
    return func(func(x))


def add_one(n: int) -> int:
    return n + 1


print(apply_twice(add_one, 5))   # 7

Callable[[int], int] reads as: “a function taking one int and returning an int”. The square brackets describe arguments, then return type.

Don’t worry about memorising this — we’ll cover it more in Section 13. The point is that Python’s type system can describe almost anything you’ll need.

When to write type hints

We’ve said throughout the course: always. Specifically:

  • Every function’s parameters and return type
  • Module-level variables whose type isn’t obvious
  • Collections whose element type can’t be inferred

Local variables inside a function can often skip the hint when their value makes the type obvious:

def main() -> None:
    name = "Manikandan"      # type is obvious — no hint needed
    ages: list[int] = []     # empty list — hint helps the type checker

Docstrings — describing what a function does

A docstring is a string written right after a function’s def line. It documents what the function does, what its parameters mean, and what it returns:

def add(a: int, b: int) -> int:
    """Return the sum of two integers."""
    return a + b

Docstrings use triple-quoted strings, so they can span multiple lines.

For more complex functions, a multi-line docstring covers parameters and return value:

def compute_average(numbers: list[float], skip_zeros: bool = False) -> float:
    """
    Return the average of a list of numbers.

    Args:
        numbers: The values to average. Must not be empty.
        skip_zeros: If True, exclude zeros from the average.

    Returns:
        The arithmetic mean as a float.

    Raises:
        ValueError: If the list is empty (or only zeros, when skip_zeros is True).
    """
    if skip_zeros:
        numbers = [n for n in numbers if n != 0]
    if not numbers:
        raise ValueError("Cannot average an empty list")
    return sum(numbers) / len(numbers)

Several styles exist (Google style, NumPy style, Sphinx style). The format above is Google style — it’s the most common in modern ML code. Pick a style and stick with it across your project.

Accessing docstrings

A function’s docstring is available at runtime via __doc__:

print(add.__doc__)
# Return the sum of two integers.

The built-in help() function prints them nicely:

help(compute_average)

This is how help() is so useful — every well-documented function carries its own usage info.

When to write docstrings

  • Every public function (the ones other parts of the code will call)
  • Every function whose name doesn’t already make the behaviour obvious
  • Every function that takes more than 2–3 parameters
  • Every function that can raise an exception

Skip docstrings for trivial one-liners:

def square(x: int) -> int:
    return x * x

The name and type hint already tell you everything.

Summary of Section 5

You can now:

  • Define functions with def
  • Type-hint parameters and return values
  • Use keyword and default arguments
  • Handle variable-length arguments with *args and **kwargs
  • Understand scope and avoid mutating globals
  • Write lambdas for tiny in-line functions
  • Write docstrings that explain what your code does

Functions are the most important building block in any program. Every section from here on will use them heavily.

What’s next

Section 6: Core Data Structures — lists, tuples, sets, and dictionaries. The single most important section in this course for anyone heading into AI or ML.

Toggle theme (T)