Skip to content
elephantoo

Functions

Lesson 11 of 38 13 min read

def, parameters and arguments, defaults, keyword-only parameters, return values, docstrings and recursion.


Functions let you give a name to a chunk of logic and reuse it. They are the building blocks of every non-trivial program: they remove repetition, make code testable, and let you think about what something does without re-reading how it does it.

Defining and calling#

Python
def greet(name):
    return f"Hello, {name}!"


print(greet("Ada"))
message = greet("Alan")
print(message.upper())
Output
Hello, Ada!
HELLO, ALAN!
  • def starts the definition, followed by the name and parameters in parentheses, then a colon.
  • The indented body runs only when the function is called.
  • return sends a value back to the caller and exits the function immediately.

A function must be defined before the line that calls it runs — which is why definitions usually sit at the top of a file and the "main" code at the bottom.

Parameters vs arguments#

Parameters are the names in the definition; arguments are the values you pass. You can pass arguments by position or by keyword:

Python
def describe_pet(name, species):
    return f"{name} is a {species}"


print(describe_pet("Rex", "dog"))                 # positional
print(describe_pet(species="cat", name="Tom"))    # keyword: order doesn't matter
Output
Rex is a dog
Tom is a cat

Keyword arguments make call sites self-documenting — connect(timeout=5) is clearer than connect(5).

Default values#

Python
def greet(name, greeting="Hello", excited=False):
    text = f"{greeting}, {name}"
    return text + ("!" if excited else ".")


print(greet("Ada"))
print(greet("Ada", "Namaste"))
print(greet("Ada", excited=True))
Output
Hello, Ada.
Namaste, Ada.
Hello, Ada!

Parameters with defaults must come after those without.

The mutable-default trap

Default values are evaluated once, when the def runs — not on every call. A mutable default like a list is therefore shared between calls:

Python
def add_item(item, basket=[]):      # dangerous!
    basket.append(item)
    return basket


print(add_item("apple"))
print(add_item("pear"))             # surprise: same list as before
Output
['apple']
['apple', 'pear']

The safe pattern uses None as the default and creates a fresh object inside:

Python
def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket


print(add_item("apple"))
print(add_item("pear"))
Output
['apple']
['pear']

Keyword-only and positional-only parameters#

A bare * in the parameter list forces everything after it to be passed by keyword. This prevents confusing calls like create_user("ada", True, False):

Python
def create_user(username, *, admin=False, active=True):
    return {"username": username, "admin": admin, "active": active}


print(create_user("ada", admin=True))
Output
{'username': 'ada', 'admin': True, 'active': True}

Calling create_user("ada", True) would raise TypeError. (A / does the opposite: parameters before it are positional-only. You'll see it in the signatures of built-ins like len(obj, /).)

Return values#

A function can return any object — including several values at once, which Python packs into a tuple:

Python
def stats(numbers):
    if not numbers:
        return None                     # early exit for the empty case
    return min(numbers), max(numbers), sum(numbers) / len(numbers)


low, high, avg = stats([4, 8, 15, 16, 23, 42])
print(low, high, round(avg, 2))
print(stats([]))
Output
4 42 18.0
None

Prefer returning values over printing inside functions. A function that returns can be tested, reused and combined; one that prints can only print.

Docstrings and type hints#

Python
def area(width: float, height: float) -> float:
    """Return the area of a rectangle.

    Both sides must be non-negative.
    """
    return width * height


print(area(3, 4.5))
print(area.__doc__.splitlines()[0])
Output
13.5
Return the area of a rectangle.

A docstring — the string on the first line of the body — documents the function; help(area) and editors display it. Type hints (: float, -> float) tell readers and tools what to pass and expect. Python does not enforce them at runtime, but checkers like mypy and your editor use them to catch bugs. There's a full lesson on type hints later.

Functions are objects#

In Python a function is a value like any other: you can store it in a variable, put it in a dict, or pass it to another function. That's what makes sorted(words, key=len) work — len is passed in, not called.

Python
def shout(text):
    return text.upper() + "!"


def whisper(text):
    return text.lower() + "..."


def apply(style, message):
    return style(message)


styles = {"loud": shout, "quiet": whisper}
print(apply(shout, "hello"))
print(styles["quiet"]("HELLO"))
Output
HELLO!
hello...

This idea — functions taking and returning functions — powers callbacks, decorators and much of Python's elegance.

Recursion#

A function can call itself. Each call must move towards a base case that stops the recursion:

Python
def factorial(n):
    if n <= 1:          # base case
        return 1
    return n * factorial(n - 1)


print(factorial(5))
Output
120

Recursion is natural for tree-shaped data (folders, nested JSON), but Python limits recursion depth to about 1000 calls, so use loops for long linear processes.

Designing good functions#

  • Do one thing. If you need "and" to describe it, split it.
  • Name it with a verb: calculate_total, send_email, is_valid.
  • Keep it short — if it doesn't fit on a screen, it's probably doing too much.
  • Avoid hidden inputs and outputs: take data as parameters and return results rather than reading or changing global variables.

Worked example: a small receipt module#

Python
TAX_RATE = 0.18


def line_total(price: float, qty: int = 1) -> float:
    """Price times quantity."""
    return price * qty


def receipt(items: list[tuple[str, float, int]], *, tax_rate: float = TAX_RATE) -> str:
    """Build a printable receipt from (name, price, qty) tuples."""
    lines = []
    subtotal = 0.0
    for name, price, qty in items:
        total = line_total(price, qty)
        subtotal += total
        lines.append(f"{name:<12}{qty:>3} x {price:>8.2f} = {total:>9.2f}")
    tax = subtotal * tax_rate
    lines.append(f"{'Tax':<29}{tax:>9.2f}")
    lines.append(f"{'TOTAL':<29}{subtotal + tax:>9.2f}")
    return "\n".join(lines)


print(receipt([("Notebook", 120, 3), ("Pen", 25.5, 4)]))
Output
Notebook      3 x   120.00 =    360.00
Pen           4 x    25.50 =    102.00
Tax                              83.16
TOTAL                           545.16

Common mistakes#

  • Forgetting to call: print(greet) prints <function greet at 0x...>; you need greet("Ada").
  • Printing instead of returning — then total = add(2, 3) gives None.
  • Code after return never runs.
  • Mutable default arguments — use None.
  • Too many positional parameters — use keyword-only parameters or group related data.

What's next#

You've seen that variables inside a function are separate from those outside. Next we'll explore exactly how that works: scope, the LEGB rule and closures.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What does a function return if there is no return statement?

  2. 2.In def greet(name, excited=False):, what is excited?

  3. 3.Why is def add(item, items=[]): a bad idea?

Finished reading?

Mark this lesson complete to track your progress.