Skip to content

Lesson 12

Python Functions

Functions package a block of code under a name so you can reuse it — the foundation of clean, readable scripts.

A function is a named block of code you define once and call as many times as you need. You’ve been calling functions since Lesson 03 — print, len, str, split, replace, sorted. This lesson is about writing your own.

Two principles to keep in mind:

  1. Don’t repeat yourself. If you find yourself copy-pasting three lines and changing one number, those lines belong inside a function.
  2. Name what code does. A well-named function turns five mysterious lines into one readable line — clean_filename(name) is more useful than the five lines that strip spaces, punctuation, and case.

Defining a function

The keyword is def, followed by the name, parentheses with any parameters, a colon, and an indented block.

def double(a):
    return a * 2

a is a parameter — a placeholder for whatever value the caller passes in. Inside the function, a behaves like any other variable. The function ends either at the first un-indented line or at a return statement.

Calling it

Use the name plus parentheses and the arguments that fill in the parameters:

def double(a):
    return a * 2

print(double(3))     # 6
result = double(7)
print(result)        # 14

You can call a function inside a loop — exactly the use case that makes them earn their keep:

def double(a):
    return a * 2

for n in [1, 2, 3]:
    print(double(n))

Returning values

return is what hands a value back to the caller. A function that doesn’t return anything implicitly returns None:

def shout(text):
    print(text.upper())          # prints, but returns nothing

x = shout("hello")
print(x)                          # None

Compare with a function that does return:

def shouting(text):
    return text.upper()

x = shouting("hello")
print(x)                          # 'HELLO'

Both versions are valid; they’re just answering different questions. print displays. return hands a value to whoever called you. Most useful functions return.

A function can return more than one value at once, separated by commas. The result is a tuple:

def name_parts(full):
    first, last = full.split(maxsplit=1)
    return first, last

f, l = name_parts("Ada Lovelace")
print(f, l)                       # Ada Lovelace

Parameters with defaults

A parameter can have a default value, used when the caller doesn’t supply one:

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}."

print(greet("Ada"))                       # 'Hello, Ada.'
print(greet("Ada", greeting="Bonjour"))   # 'Bonjour, Ada.'

Defaults make a function easy to use for the common case without locking out the unusual one.

Keyword arguments — call by name

You can pass arguments by position (in the order the parameters were defined) or by name:

def slice_text(text, start, end):
    return text[start:end]

print(slice_text("digital humanities", 0, 7))               # by position
print(slice_text(text="digital humanities", start=0, end=7))  # by name

For a function with two or three obvious parameters, positional is fine. As soon as a function has four or more, naming arguments makes calls vastly more readable — you can tell what slice_text(text, 0, 7) is doing at a glance.

Modern Python code annotates parameters and return types. The annotations don’t change what the code does — Python is still dynamic — but they make the function self-documenting and catch real bugs when used with a checker like mypy or pyright.

def double(a: int) -> int:
    return a * 2

def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}."

You don’t have to use type hints to write Python well. But once you start, you’ll find your own functions easier to come back to in three months.

Docstrings — describe what the function does

A string at the top of a function body becomes its docstring — visible in editors, tools, and help(). Triple quotes, one line for short ones, multiple lines if a function deserves explanation:

def slice_text(text: str, start: int, end: int) -> str:
    """Return text[start:end]. Indexes follow Python's half-open slice rule."""
    return text[start:end]

For a one-liner you’d otherwise comment, this is strictly better — the documentation lives with the code and shows up where it’s useful.

Empty functions — pass

Python won’t accept an empty block. When you’re sketching out a script and want a function you’ll fill in later, use pass:

def parse_letter(text):
    pass    # TODO: extract sender, date, body

pass is also useful in if and while blocks where you need a placeholder but no action.

Scope — what a function can see

A variable defined inside a function is local to that function. It doesn’t exist outside, and it doesn’t leak into other functions:

def f():
    x = 10
    return x

print(f())
print(x)    # NameError: name 'x' is not defined

A function can read variables defined in the enclosing module, but it should be rare and deliberate. The cleaner pattern is to pass everything a function needs in as parameters and return what it produces. Functions that depend on hidden global state are hard to test and harder to reuse.

A worked example — cleaning text

A small function that does something real: strip punctuation and lowercase a string, the kind of preprocessing you’ll do constantly in text analysis.

import string

def normalize(text: str) -> str:
    """Lowercase the text and strip ASCII punctuation."""
    no_punct = text.translate(str.maketrans("", "", string.punctuation))
    return no_punct.lower()

print(normalize("The quality of mercy, is not strain'd."))
# 'the quality of mercy is not straind'

That’s three lines of work behind one descriptive name. From here on, anywhere you’d otherwise repeat the punctuation-stripping logic, you call normalize(text) instead. That’s the whole game.

When you can write functions comfortably, move on to Lesson 13: Python Classes.

Running the code

Save any snippet from this lesson to a file — say try.py — and run it from your project folder:

uv run try.py

uv run uses the project’s Python and dependencies automatically; no virtualenv to activate. If you haven’t set the project up yet, Lesson 01 walks through it.