Functions

As your programs grow, you will write the same logic in more than one place. Functions let you write logic once, name it, and use it everywhere. Fix it in one place and every call gets the fix automatically.
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # "Hello, Alice!"
print(greet("Bob")) # "Hello, Bob!"Write it once, use it everywhere, fix it in one place.
Defining a function
The def keyword starts a function definition, followed by the name, parentheses, a colon, and an indented body. A function does nothing until you call it. Define it with def, then call it by name with ().
def say_hello():
print("Hello!")
say_hello() # call the functiondef name(): and indent the body underneath. Nothing happens until you call it with name(), the def only sets it up. And if you forget a return, the function quietly hands back None, which caught me out more than once early on. Parameters and arguments
Parameters are the inputs your function expects. List them inside the parentheses. When you call the function, the values you pass are matched to the parameters in order.
def greet(name, greeting):
print(f"{greeting}, {name}!")
greet("Alice", "Hello") # "Hello, Alice!"
greet("Bob", "Hi") # "Hi, Bob!"Parameter is the name in the function definition. Argument is the actual value you pass when you call it. In practice people use the words interchangeably; the distinction mainly matters when reading docs.
Default values
You can give parameters a default value. If the caller does not provide that argument, the default is used. Parameters with defaults must come after parameters without defaults.
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # "Hello, Alice!"
greet("Alice", "Hi") # "Hi, Alice!"Parameters with defaults must come after parameters without defaults.
Keyword arguments
When calling a function, you can name the arguments. This makes calls readable, especially for functions with many parameters, and lets you pass them in any order.
def describe_player(name, score, level):
print(f"{name} | Score: {score} | Level: {level}")
describe_player("Alice", 87, 5) # positional
describe_player(name="Alice", level=5, score=87) # keyword, any order
describe_player("Alice", level=5, score=87) # mix: positional firstscore=87) and the call explains itself, plus you can pass them in any order. The one rule: any positional arguments come before the named ones. Great for functions with a pile of parameters. Return values
return sends a value back to the caller. Without return, a function gives back None. Once return runs, the function exits immediately. Any code after it in that block is skipped.
def add(a, b):
return a + b
result = add(3, 4) # result = 7
print(result)return also exits the function immediately. Any code after it in that block does not run.
return hands a value back to whoever called the function, and the function stops right there, anything after it is skipped. Leave out return and you get None back. print shows a value, return gives it back to use, took me a while to feel that difference. Returning multiple values
Python lets you return multiple values by separating them with commas. The caller receives them as a tuple and can unpack them into separate names in one line.
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 7, 1, 9, 4])
print(low, high) # 1 9The low, high = ... syntax is unpacking: Python assigns each returned value to the corresponding name.
low, high = min_max(...), one name per value. Handy whenever a function naturally produces more than one answer. Scope
Variables created inside a function exist only inside that function. You cannot see them from outside. Variables defined outside all functions are visible everywhere, but you cannot change them from inside a function without an explicit declaration.
global line first. Reach for that almost never: pass values in, return results out, and the function stays simple to follow. def calculate():
result = 42 # local to this function
return result
calculate()
print(result) # NameError, result doesn't exist out herecount = 0
def increment():
global count # declare you want to modify the global
count += 1
increment()
print(count) # 1Using global should be a last resort. It makes code harder to reason about. Prefer passing values in and returning them out. Scope builds directly on how assignment binds a name to a value, covered in the Variables and types chapter.
*args and **kwargs
Sometimes you do not know how many arguments a function will receive. *args collects any number of positional arguments into a tuple. **kwargs collects any number of keyword arguments into a dictionary. The names args and kwargs are conventions; the stars are what matter.
def total(*args):
return sum(args)
total(1, 2, 3) # 6
total(1, 2, 3, 4, 5) # 15def display(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
display(name="Alice", score=87, level=5)You can mix them with regular parameters. Regular parameters come first:
def describe(title, *tags, **metadata):
print(f"{title} | tags: {tags} | meta: {metadata}")
describe("Python intro", "beginner", "python", author="Alice", year=2024)*args scoops up any number of extra positional arguments as a tuple, **kwargs scoops up extra named ones as a dict. The words args and kwargs are only convention, the * and ** do the real work. Handy when you can't say upfront how many things come in. Docstrings
A docstring is a string at the top of a function that describes what it does. Python editors and tools use it to show help when you hover over a function call. Use triple quotes, and write one line for simple functions.
def normalise(value, min_val, max_val):
"""Scale a value to the 0-1 range given the known min and max."""
return (value - min_val) / (max_val - min_val)def build_url(base, version, resource, *, secure=True):
"""
Build an API endpoint URL.
Returns a fully-qualified URL string. If secure is False,
the URL will use http instead of https.
"""
scheme = "https" if secure else "http"
base = base.replace("https://", "").replace("http://", "")
return f"{scheme}://{base}/{version}/{resource}"Write a docstring for any function whose purpose is not clear from its name and signature.
Type hints
Type hints let you annotate what types a function expects and returns. Python does not enforce them at runtime, but editors use them to catch mistakes before you run anything. The -> before the colon specifies the return type.
def greet(name: str, score: int) -> str:
return f"{name} scored {score}"def log(message: str) -> None:
print(f"[LOG] {message}")def top_scores(scores: list[int], n: int) -> list[int]:
return sorted(scores, reverse=True)[:n]Type hints are optional but valuable on any function that will be called from multiple places. They are documentation that tools can verify.
name: str and -> str for the return. Python won't enforce them when it runs, but your editor reads them and warns you before you hit run. Think of them as notes the tools can check. Functions as values
Functions in Python are values, like strings or numbers. You can assign them to variables and pass them to other functions. This is how sorted() accepts a key= function.
def double(x):
return x * 2
def apply(func, value):
return func(value)
apply(double, 5) # 10Passing functions as arguments shows up constantly with sorted(), map(), and filter(). You will also see it in the Lambdas and comprehensions chapter.
key= to sorted(). Felt strange to me at first, then it clicked and turned up everywhere. In practice
Two functions that work together: letter_grade converts a score to a letter, and summarise calls it for every score in a list:
def letter_grade(score: int) -> str:
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
def summarise(scores: list[int]) -> None:
total = sum(scores)
avg = total / len(scores)
grades = []
for s in scores:
grades.append(letter_grade(s))
print(f"Average: {avg:.1f}")
print(f"Grades: {', '.join(grades)}")
summarise([87, 92, 74, 65, 91])
