Elena' s AI Blog

Python Functions: Writing Reusable Code

12 May 2026 (updated: 27 Jul 2026) / 18 minutes to read

Elena Daehnhardt


Generated by Midjourney. Prompt: A developer focused at her terminal — coding in flow state.


TL;DR:
  • Functions let you write code once and reuse it everywhere. Learn def, parameters, return values, default arguments, *args/**kwargs, type hints, and lambda expressions — the tools that make your Python programs clean, readable, and maintainable.

Previous: Part 1 — Python Programming Language

Next: Part 3 — Python Error Handling: When Birds Misbehave

Python Functions: Why Reusable Code Matters

In my first Python post we covered variables, lists, dictionaries, and list comprehensions — the data and control flow that let you write a working script. In the OOP post we jumped all the way to classes. But there is an important stop in between, and that stop is functions.

Functions are how you stop writing the same thing twice. Functions are the reason a 500-line program does not become a 5,000-line program, and they represent the first step toward thinking about code as something you design rather than something you just write. Once functions feel natural, classes make much more sense — a class is largely just a collection of functions that share some data.

The birds stay in this post too — patient, useful, and by now familiar.

What Is a Function in Python?

A function is a named, reusable block of code that you can call by name, pass data into, and get a result back from. In Python you define one with def:

def greet_bird(name):
    print(f"Hello, {name}!")

greet_bird("Eagle")
greet_bird("Pigeon")
Hello, Eagle!
Hello, Pigeon!

Writing the logic once inside def and calling it as many times as needed is the whole idea behind functions. Without functions we would have to repeat print(f"Hello, ...") every time — tedious, error-prone, and hard to change later.

Python Function Parameters and Return Values

Functions become genuinely useful when they take inputs and give something back. The return statement sends a value back to the caller:

def describe_bird(name, color, can_fly):
    status = "can fly" if can_fly else "cannot fly"
    return f"A {color} {name} that {status}."

print(describe_bird("Penguin", "black and white", False))
print(describe_bird("Eagle", "brown", True))
A black and white Penguin that cannot fly.
A brown Eagle that can fly.

The variables name, color, and can_fly are called parameters — placeholders that receive the values you pass in. The values you pass when calling the function ("Penguin", "black and white", False) are called arguments. A parameter is the named slot declared in the signature; an argument is the concrete value supplied at the call site.

A function can return any Python object: a number, a string, a list, a dictionary, even another function. If you do not write a return statement, Python returns None silently.

Python Default Arguments and the Mutable Default Gotcha

Sometimes a parameter has a sensible default value and you do not want to have to type it every time. Python lets you set defaults in the function signature:

def feed_bird(name, food="seeds", amount=100):
    print(f"Feeding {name} with {amount}g of {food}.")

feed_bird("Pigeon")
feed_bird("Parrot", food="fruit")
feed_bird("Eagle", food="fish", amount=300)
Feeding Pigeon with 100g of seeds.
Feeding Parrot with 100g of fruit.
Feeding Eagle with 300g of fish.

SyntaxError: non-default argument follows default argument

Default arguments must come after non-default ones in the signature. Putting a defaulted parameter before a required one raises this exact error at definition time:

# Wrong — default before non-default
def feed_bird(food="seeds", name):  # SyntaxError: non-default argument follows default argument
    ...

Fix: reorder the parameters so every parameter with a default value comes after all parameters without one, as in the feed_bird(name, food="seeds", amount=100) signature above.

Mutable Default Arguments in Python

Never use a mutable object (a list, a dictionary) as a default argument. Python evaluates the default value once, when the def statement runs, not on each call, so every call that omits the argument shares the same object and mutations accumulate in surprising ways. Use None and create the object inside the function instead — see the official Python FAQ entry on this exact behavior:

# Wrong
def add_bird(bird, flock=[]):
    flock.append(bird)
    return flock

# Right
def add_bird(bird, flock=None):
    if flock is None:
        flock = []
    flock.append(bird)
    return flock

Python *args and **kwargs: Variable-Length Arguments

Sometimes a function needs to accept a variable number of arguments. Two special syntaxes handle this: *args and **kwargs.

*args collects any number of positional arguments into a tuple:

def count_birds(*birds):
    print(f"You have {len(birds)} birds: {', '.join(birds)}.")

count_birds("Eagle", "Pigeon")
count_birds("Eagle", "Pigeon", "Stork", "Swan", "Penguin")
You have 2 birds: Eagle, Pigeon.
You have 5 birds: Eagle, Pigeon, Stork, Swan, Penguin.

**kwargs collects any number of keyword arguments into a dictionary:

def bird_profile(name, **attributes):
    print(f"\n{name}:")
    for key, value in attributes.items():
        print(f"  {key}: {value}")

bird_profile("Eagle", color="brown", wingspan=200, can_fly=True)
bird_profile("Penguin", color="black and white", can_swim=True, speed_kmh=3)
Eagle:
  color: brown
  wingspan: 200
  can_fly: True

Penguin:
  color: black and white
  can_swim: True
  speed_kmh: 3

*args and **kwargs can combine with regular parameters in one function signature. The required order is:

  • regular positional parameters
  • *args
  • keyword-only parameters
  • **kwargs
def full_report(location, *birds, season="summer", **notes):
    print(f"Location: {location}, Season: {season}")
    print(f"Birds spotted: {', '.join(birds)}")
    for key, value in notes.items():
        print(f"  Note — {key}: {value}")

full_report("Amsterdam", "Heron", "Coot", season="spring", weather="rainy")
Location: Amsterdam, Season: spring
Birds spotted: Heron, Coot
  Note — weather: rainy

Python Type Hints for Function Parameters and Return Values

Python is dynamically typed, which means a function will happily accept the wrong type of argument and fail later in a confusing way. A Python type hint is an optional annotation that documents the types a function expects and returns without being enforced by the interpreter at runtime. Type hints make code much easier to read and let tools like mypy or your IDE catch type errors before you run anything:

def calculate_flight_time(distance_km: float, speed_kmh: float) -> float:
    """Return flight time in hours for a given distance and speed."""
    return distance_km / speed_kmh

hours = calculate_flight_time(200.0, 55.0)
print(f"Flight time: {hours:.2f} hours")
Flight time: 3.64 hours

The -> float after the parentheses declares the return type. The docstring inside triple quotes documents what the function does — a habit worth forming from the beginning, because future-you will thank present-you.

For more complex types you can import from typing (Python 3.8 and earlier) or use the built-in generics (Python 3.9+):

# Python 3.9+
def filter_flying_birds(birds: list[str], can_fly: dict[str, bool]) -> list[str]:
    return [b for b in birds if can_fly.get(b, False)]

birds = ["Eagle", "Penguin", "Pigeon", "Ostrich"]
flight_map = {"Eagle": True, "Penguin": False, "Pigeon": True, "Ostrich": False}
print(filter_flying_birds(birds, flight_map))
['Eagle', 'Pigeon']

Python Lambda Functions: Anonymous Single-Expression Functions

A lambda is a small anonymous function written in a single expression — a function defined without a name via the def keyword. A lambda is useful when a short function is needed as an argument to another function and a full def would be overkill for something so brief:

birds = [
    {"name": "Eagle",   "wingspan": 200},
    {"name": "Pigeon",  "wingspan": 50},
    {"name": "Stork",   "wingspan": 110},
    {"name": "Swan",    "wingspan": 240},
]

# Sort by wingspan using a lambda as the key
sorted_birds = sorted(birds, key=lambda b: b["wingspan"])
for bird in sorted_birds:
    print(f"{bird['name']}: {bird['wingspan']} cm")
Pigeon: 50 cm
Stork: 110 cm
Eagle: 200 cm
Swan: 240 cm

Use lambda sparingly: a lambda is appropriate for the classification of a single-expression, throwaway function passed inline. When the logic is more than a single expression, a named def is always clearer. A well-named function is its own documentation; a complex lambda is just confusion waiting to happen.

Combining Parameters, *args, and **kwargs: A Bird Feeding Station Example

Here is a slightly more realistic example that combines default arguments, *args, keyword-only parameters, **kwargs, and type hints from this post — a simple bird feeding station manager:

from datetime import datetime

def record_feeding(
    bird_name: str,
    food: str = "seeds",
    amount_g: float = 100.0,
    *observers: str,
    location: str = "garden",
    **weather,
) -> dict:
    """
    Record a bird feeding event and return a log entry.

    Args:
        bird_name: Name of the bird being fed.
        food: Type of food provided.
        amount_g: Amount of food in grams.
        *observers: Names of people who witnessed the feeding.
        location: Where the feeding took place.
        **weather: Arbitrary weather conditions as key-value pairs.

    Returns:
        A dictionary representing the feeding log entry.
    """
    return {
        "timestamp": datetime.now().isoformat(),
        "bird": bird_name,
        "food": food,
        "amount_g": amount_g,
        "observers": list(observers),
        "location": location,
        "weather": weather,
    }

entry = record_feeding(
    "Pigeon",
    "mixed grain",
    150.0,
    "Elena", "Anna",
    location="park",
    temperature_c=18,
    wind="light breeze",
)

for key, value in entry.items():
    print(f"{key}: {value}")
timestamp: 2026-05-10T09:14:02.341
bird: Pigeon
food: mixed grain
amount_g: 150.0
observers: ['Elena', 'Anna']
location: park
weather: {'temperature_c': 18, 'wind': 'light breeze'}

One function, fully documented, handles a wide range of inputs, and returns structured data ready to be stored, logged, or passed to the next function in your program.

Python Functions: Key Takeaways

A Python function represents a reusable unit of logic that turns a collection of lines into a program you can actually maintain and build on — the feature that feels slightly unnecessary when you first learn it and indispensable once you have used it for a week.

In the next post in this series we will look at what happens when things go wrong: error handling, exceptions, and how to write Python that fails gracefully rather than crashing in the most confusing way possible. The birds will be involved, and some of them will misbehave.

Did you like this post? Please let me know if you have any comments or suggestions — always happy to hear from you!

Python Functions FAQ

What is the difference between a parameter and an argument in Python?

A parameter is the named placeholder declared in a function’s signature (name, color, can_fly); an argument is the actual value passed in when the function is called ("Penguin", "black and white", False). The signature defines parameters once; every call site supplies arguments.

Why shouldn’t I use a mutable default argument in Python?

Python evaluates default argument values once, when the function is defined, not on every call. A mutable default such as flock=[] is therefore the same list object shared across every call that does not supply its own, so mutations from one call leak into the next. Use flock=None and create the list inside the function body instead.

What is the correct order for regular parameters, *args, and **kwargs in a Python function signature?

A Python function signature must declare parameters in this order: regular positional parameters, then *args, then keyword-only parameters, then **kwargs. For example def full_report(location, *birds, season="summer", **notes) is valid; putting **kwargs before *args raises a SyntaxError.

Are Python type hints enforced at runtime?

No. Python type hints are optional annotations that document expected argument and return types; the interpreter does not check or enforce them when the function runs. Static analysis tools such as mypy read the same annotations and catch type mismatches before the code executes.

When should I use a lambda instead of a def function in Python?

Use a lambda only for a short, single-expression function passed as an argument to another function, such as the key argument of sorted(). Once the logic needs more than one expression or a name that documents its purpose, a named def function is clearer and easier to debug.

References

  1. Python Documentation — Defining Functions
  2. PEP 484 — Type Hints
  3. PEP 3107 — Function Annotations
  4. Python Basics — Syntax and Data Structures
  5. Python Classes and OOP
  6. mypy — Optional Static Typing for Python
  7. Python FAQ — Why Are Default Values Shared Between Objects?
desktop bg dark

About Elena

Elena, a PhD in Computer Science, simplifies AI concepts and helps you use machine learning.

Citation
Elena Daehnhardt. (2026) 'Python Functions: Writing Reusable Code', daehnhardt.com, 12 May 2026. Available at: https://daehnhardt.com/blog/2026/05/12/python-functions/
All Posts