PythonBeginner 15 min Lesson 9 of 30

Day 9 — Functions

Package logic into reusable functions. Learn parameters, return values, default arguments, scope, and the mutable default trap.

Python · Lesson 9 of 30
0/30 done(0%)

What is it? #

A function is a named block of code you can run whenever you need it. You define it once with def, then call it as many times as you like.

Functions take inputs, called parameters, and hand back an output with return. A function without a return gives you None.

The point is not just avoiding repetition. A good function also gives a name to an idea, so the calling code reads like a description of what happens rather than a wall of steps.

Names created inside a function are local to it. They disappear when the function ends and cannot be seen from outside. That isolation is what makes functions safe to reuse.

Think of it like this #

A function is a kitchen appliance. A blender does not care whose fruit goes in; you hand it ingredients, it does its one job, and it gives you back a result. You do not need to know how the motor works to use it, and the mess stays inside the jug.

Simple example #

You keep calculating the final price of an order: subtotal, discount, tax, shipping. Instead of repeating those four lines in five places, you write one function with sensible defaults and call it everywhere.

Code #

PYTHON
def final_price(subtotal, discount=0.0, tax_rate=0.18, shipping=0.0):
    """Return the amount a customer actually pays."""
    discounted = subtotal - discount
    tax = discounted * tax_rate
    return round(discounted + tax + shipping, 2)


print(final_price(1000))                          # 1180.0
print(final_price(1000, discount=100))            # 1062.0
print(final_price(1000, shipping=50, tax_rate=0)) # 1050.0

# Returning more than one value
def split_name(full_name):
    parts = full_name.strip().split(" ", 1)
    first = parts[0]
    last = parts[1] if len(parts) > 1 else ""
    return first, last

first, last = split_name("  Anita Desai ")
print(first, "|", last)                           # Anita | Desai

# The mutable default trap
def add_tag_broken(tag, tags=[]):        # created ONCE, shared forever
    tags.append(tag)
    return tags

print(add_tag_broken("a"))               # ['a']
print(add_tag_broken("b"))               # ['a', 'b']  — surprise

def add_tag(tag, tags=None):             # the correct pattern
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

print(add_tag("a"), add_tag("b"))        # ['a'] ['b']

How it works #

def final_price(subtotal, discount=0.0, ...) defines four parameters. subtotal is required; the other three have defaults, so callers only pass what differs from the usual case.

The triple-quoted line is a docstring. It is the standard way to say what a function returns, and tools and editors read it.

final_price(1000, discount=100) uses a keyword argument. Naming arguments at the call site makes the code readable and protects you from breaking calls when parameter order changes later.

return round(..., 2) sends one value back. Execution of the function stops at return — anything after it never runs.

split_name returns two values. Python packs them into a tuple and the caller unpacks them, which is why first, last = split_name(...) works.

The last block is the classic Python gotcha. Default values are evaluated once, when the function is defined, not on each call. So tags=[] creates a single list that every call shares, and it keeps growing. The fix is None as the default and creating a fresh list inside. Make this a habit: never use a list, dict or set as a default value.

Real-world use #

Functions are the first and most important unit of structure in any codebase. Before classes, before modules, before architecture, there are functions with clear names and honest return values.

A function that is easy to test is usually a function that takes its inputs as parameters and returns a result, rather than reading global state and printing. That is why "pass data in, return data out" is the default advice — it makes unit testing on Day 23 almost free.

Keyword arguments are heavily used in real APIs and libraries, because a call like send_email(to=user.email, subject=subject, html=body, retry=True) documents itself. Positional arguments beyond two or three quickly become unreadable.

Common mistakes #

  • Using a list or dictionary as a default argument. It is created once and shared across all calls — use None and build it inside.
  • Forgetting return, then wondering why the caller got None.
  • Writing a function that both computes and prints. Return the value; let the caller decide how to display it.
  • Depending on global variables instead of parameters. It makes the function impossible to test in isolation.
  • Letting a function grow past a screen. If you need a comment to mark sections inside it, those sections want to be functions.

Practice #

Write monthly_emi(principal, annual_rate, months) that returns the monthly instalment, rounded to two decimals. Give annual_rate a sensible default. Then write summarise(loans) that takes a list of loan tuples and returns both the total and the average instalment.

Quick quiz

  1. 1. What does a function return if it has no `return` statement?

  2. 2. Why is `def f(items=[])` risky?

  3. 3. What happens to variables created inside a function?

  4. 4. How does a function return two values?

  5. 5. Why prefer keyword arguments at call sites?

Summary

  • A function names an idea and makes it reusable; `def` defines it and `return` hands a value back.
  • Default parameters let callers pass only what differs from the normal case.
  • Never use mutable defaults — use None and create the value inside.
  • Keep functions small, take inputs as parameters and return results rather than printing.
  • Local names stay local, which is what makes functions safe to reuse and easy to test.