Defining and Calling Functions

A function packages a piece of work behind a name. def creates it, the call runs it, and return decides what comes back - which is None if you never say.

Defining a function

def greet(name):
    """Return a greeting for the given name."""
    return f"Hello, {name}"


message = greet("Meera")
print(message)              # Hello, Meera
PartMeaning
defThe keyword that creates a function object.
greetThe name the function object is bound to.
(name)The parameter list. These are local names, filled in at call time.
:Opens the body.
the docstringOptional, but the first thing anyone reads.
returnEnds the function and hands a value back.
def is a statement that runs. It creates a function object and binds a name to it. Calling a function before the def has executed is a NameError, which is why functions are defined before the code that uses them.

Parameters and arguments

def rectangle_area(width, height):     # width and height are PARAMETERS
    return width * height


print(rectangle_area(4, 5))            # 4 and 5 are ARGUMENTS

Parameters are the names in the definition. Arguments are the values supplied at the call. The distinction matters when reading error messages.

return

def add(a, b):
    return a + b


def show(a, b):
    print(a + b)                # prints, but returns nothing


result_1 = add(2, 3)
result_2 = show(2, 3)

print(result_1)                 # 5
print(result_2)                 # None

A function with no return, or with a bare return, gives back None. Printing and returning are different actions: printing sends text to the screen, returning hands a value to the caller. A function that only prints cannot be reused in a calculation.

return exits immediately

def classify(n):
    if n < 0:
        return "negative"
    if n == 0:
        return "zero"
    return "positive"
    print("never runs")         # unreachable


print(classify(-5))             # negative

Returning several values

def statistics(values):
    return min(values), max(values), sum(values) / len(values)


low, high, mean = statistics([4, 8, 15])
print(low, high, mean)          # 4 15 9.0

That is one tuple, unpacked at the call site.

Docstrings

def net_price(amount, tax_rate=0.18):
    """Return the amount including tax.

    Args:
        amount: The pre-tax amount.
        tax_rate: The tax rate as a fraction. Defaults to 0.18.

    Returns:
        The amount plus tax, as a float.
    """
    return amount * (1 + tax_rate)


print(net_price.__doc__)
help(net_price)

A docstring is a string literal as the first statement of the function. Unlike a comment it is kept at runtime, which is what makes help() and editor tooltips work. Write one for every function that anyone else will call.

Functions are objects

def double(n):
    return n * 2


print(type(double))          # <class 'function'>
print(double.__name__)       # double

twice = double               # a second name for the same function
print(twice(5))              # 10

operations = [double, abs, len]
print(operations[0](7))      # 14

print(list(map(double, [1, 2, 3])))     # [2, 4, 6]

A function can be stored in a variable, put in a list, passed to another function and returned from one. This is what "first class" means, and the advanced functions note builds on it.

How arguments are passed

def rebind(items):
    items = [9, 9]          # rebinds the LOCAL name only
    return items


def mutate(items):
    items.append(9)         # changes the object the caller passed


original = [1, 2]
rebind(original)
print(original)             # [1, 2] - unchanged

mutate(original)
print(original)             # [1, 2, 9] - changed

Python passes the reference by value. The function gets its own name pointing at the caller's object. Rebinding that name affects nothing outside; mutating the object it points at affects everyone. This is neither "by value" nor "by reference" in the classical sense, and remembering the two examples above is more useful than remembering a label for it.

How to avoid surprising the caller

def add_tax(prices):
    return [p * 1.18 for p in prices]      # returns a new list


def add_tax_in_place(prices):
    for i in range(len(prices)):
        prices[i] *= 1.18                  # changes the caller's list

Prefer the first form. A function that returns a new value is easier to test, easier to reuse and impossible to misuse by accident. Mutate the argument only when that is the documented purpose of the function.

Writing good functions

# Does too much
def process(data):
    cleaned = [d.strip().lower() for d in data if d.strip()]
    counts = {}
    for item in cleaned:
        counts[item] = counts.get(item, 0) + 1
    top = sorted(counts.items(), key=lambda p: -p[1])[:3]
    print("Top three:")
    for word, count in top:
        print(f"  {word}: {count}")
    return top
# One job each
def clean(data):
    return [d.strip().lower() for d in data if d.strip()]


def count_items(items):
    counts = {}
    for item in items:
        counts[item] = counts.get(item, 0) + 1
    return counts


def top_n(counts, n=3):
    return sorted(counts.items(), key=lambda p: -p[1])[:n]


def report(top):
    print("Top three:")
    for word, count in top:
        print(f"  {word}: {count}")


words = clean(["  Apple ", "apple", "Fig", "", "fig", "fig"])
report(top_n(count_items(words)))

Each piece can now be tested alone, and only the last one touches the screen. Keeping input, computation and output in separate functions is the most valuable habit in this note.

Type hints

def net_price(amount: float, tax_rate: float = 0.18) -> float:
    return amount * (1 + tax_rate)


print(net_price(100))              # 118.0
print(net_price("abc"))            # still runs, then fails inside

Annotations are documentation that tools can read. Python does not enforce them at runtime; a separate type checker does. The type hints note covers this in full.

Common mistakes

  • Forgetting return and wondering why the caller has None.
  • Using print where return was needed, making the function unusable in a calculation.
  • Calling a function before its def has run.
  • Mutating an argument the caller did not expect to be changed.
  • Writing a function that does four things, so it can never be reused.
  • Shadowing a built in: naming a function list, sum or input.

Best practices

  • One job per function, and a name that says what that job is.
  • Return values; print only in the functions whose purpose is output.
  • Write a docstring for anything another person will call.
  • Prefer returning a new object over modifying an argument.
  • Keep a function short enough to read without scrolling.

Practice

  1. Write a function that returns the average of a list and handles the empty list case explicitly.
  2. Show two functions that take a list, one that mutates it and one that does not, and prove the difference.
  3. Split a function that reads input, computes and prints into three functions.
  4. Explain why print(f()) shows None for a function that itself prints.
  5. Write a documented function with a default argument and read its docstring at runtime.

Conclusion

A function is an object created by def, called by name, and defined by what it returns. Give each one a single job, return rather than print, and be deliberate about whether it changes the arguments it is given.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Scope and the LEGB Rule

Python resolves a name by looking in four places in a fixed order: local, enclosing, global, built-in. Assignment anywhere in a function makes the nam...

Read more
Python

Lambda Functions

A lambda is a small anonymous function written as a single expression. It exists for the places where naming a function would add nothing.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.