Python · lesson 8 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Python Functions and Parameters
Define Python functions with positional, keyword, default, *args and **kwargs parameters, see the mutable-default trap, lambdas and return values run.
By SkillByExample · Updated
A function names a block of logic so you can call it from many places and change it in one. Output below is from Python 3.13.
Defining Functions
def creates a function; the optional type hints document what it takes and returns.
# functions.py
def calculate_area(width: float, height: float) -> float:
"""Calculate the area of a rectangle.
The docstring is accessible via help() and shows up in editor tooltips.
It's the primary place to document what a function does and why.
"""
return width * height
area = calculate_area(5.0, 10.0)
print(area)
$ python functions.py
50.0
def itself runs nothing — the body executes only when calculate_area(5.0, 10.0) is called, and return hands the product back to the caller, which stores it in area.
Arguments and Parameters
Positional vs Keyword Arguments
Arguments can be matched by position or by parameter name.
# positional_keyword.py
def calculate_area(width: float, height: float) -> float:
return width * height
print(calculate_area(5.0, 10.0))
print(calculate_area(height=10.0, width=5.0))
print(calculate_area(5.0, height=10.0))
$ python positional_keyword.py
50.0
50.0
50.0
All three calls are identical to Python: keyword arguments are matched by name, so their order doesn’t matter, and you can mix the two as long as positional arguments come first.
Default Parameter Values
A default value makes a parameter optional.
# defaults.py
def greet(name: str, greeting: str = "Hello") -> str:
"""Greet a person, using 'Hello' unless a custom greeting is provided."""
return f"{greeting}, {name}!"
print(greet("Alice"))
print(greet("Alice", "Hi"))
print(greet("Alice", greeting="Hey"))
$ python defaults.py
Hello, Alice!
Hi, Alice!
Hey, Alice!
The first call fell back to "Hello"; the other two overrode it, by position and by name. Parameters with defaults must come after those without — def greet(greeting="Hello", name) is a SyntaxError.
Variadic Arguments (*args and **kwargs)
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict.
# variadic.py
def sum_values(*args: float) -> float:
print("args =", args)
return sum(args)
print(sum_values(1, 2, 3))
print(sum_values())
def print_profile(**kwargs) -> None:
print("kwargs =", kwargs)
for key, value in kwargs.items():
print(f" {key}: {value}")
print_profile(name="Alice", age=30, role="admin")
numbers = [4, 5, 6]
print(sum_values(*numbers)) # * at the call site unpacks a list
$ python variadic.py
args = (1, 2, 3)
6
args = ()
0
kwargs = {'name': 'Alice', 'age': 30, 'role': 'admin'}
name: Alice
age: 30
role: admin
args = (4, 5, 6)
15
Calling with no arguments is legal — args is just an empty tuple. The same * symbol means “collect” in a signature and “spread” at a call site, so sum_values(*numbers) passed three separate arguments, not one list.
Positional-only and keyword-only parameters
A bare * in a signature makes everything after it keyword-only; a / makes everything before it positional-only.
# markers.py
def create_user(name: str, age: int, /, *, role: str = "user", active: bool = True) -> dict:
return {"name": name, "age": age, "role": role, "active": active}
print(create_user("Alice", 30))
print(create_user("Bob", 25, role="admin"))
for call in ('create_user("Carol", 22, "mod")', 'create_user(name="Dan", age=40)'):
try:
eval(call)
except TypeError as e:
print("TypeError:", e)
$ python markers.py
{'name': 'Alice', 'age': 30, 'role': 'user', 'active': True}
{'name': 'Bob', 'age': 25, 'role': 'admin', 'active': True}
TypeError: create_user() takes 2 positional arguments but 3 were given
TypeError: create_user() got some positional-only arguments passed as keyword arguments: 'name, age'
The * stops a caller from writing create_user("Carol", 22, "mod"), where nobody reading the call can tell what "mod" means. The / does the reverse: the parameter names name and age are private to the function, so they can be renamed later without breaking callers.
The mutable default trap
Default values are evaluated once, when def runs — not on each call.
# mutable_default.py
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("apple"))
print(add_item("pear"))
def add_item_fixed(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item_fixed("apple"))
print(add_item_fixed("pear"))
$ python mutable_default.py
['apple']
['apple', 'pear']
['apple']
['pear']
The second call “remembered” the apple because both calls shared the single list created at definition time. Using None as the default and creating the list inside the function gives every call a fresh one.
Lambda Functions
A lambda is an anonymous single-expression function, most useful as a short key= callback.
# lambdas.py
double = lambda x: x * 2
print(double(5), double.__name__)
users = [
{"name": "Charlie", "age": 35},
{"name": "alice", "age": 28},
{"name": "Bob", "age": 32},
]
print([u["name"] for u in sorted(users, key=lambda u: u["age"])])
print(sorted(u["name"] for u in users))
print(sorted((u["name"] for u in users), key=str.lower))
$ python lambdas.py
10 <lambda>
['alice', 'Bob', 'Charlie']
['Bob', 'Charlie', 'alice']
['alice', 'Bob', 'Charlie']
Plain sorted put alice last because uppercase letters sort before lowercase ones in Unicode. Passing key=str.lower fixes that — and shows you often don’t need a lambda at all when an existing function already does the job. A lambda’s __name__ is just <lambda>, which is why named functions read better in tracebacks.
Return Values
A function without a return statement returns None; returning several values actually returns one tuple.
# returns.py
def min_max(numbers: list[float]) -> tuple[float, float]:
return min(numbers), max(numbers)
result = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(result, type(result).__name__)
low, high = result
print(f"Min: {low}, Max: {high}")
def log(message: str):
print("LOG:", message)
print(log("saved"))
$ python returns.py
(1, 9) tuple
Min: 1, Max: 9
LOG: saved
None
min_max returned a single tuple, which the caller unpacked. log printed its message and then implicitly returned None — which the outer print displayed.
Docstrings
A string literal right after def becomes the function’s __doc__, which help() and editors display.
# docstrings.py
def divide(a: float, b: float) -> float:
"""Divide a by b.
Raises:
ValueError: If b is zero.
"""
if b == 0:
raise ValueError("Cannot divide by zero.")
return a / b
print(divide.__doc__.splitlines()[0])
print(divide.__annotations__)
try:
divide(1, 0)
except ValueError as e:
print("ValueError:", e)
try:
divide("6", 2)
except TypeError as e:
print("TypeError:", e)
$ python docstrings.py
Divide a by b.
{'a': <class 'float'>, 'b': <class 'float'>, 'return': <class 'float'>}
ValueError: Cannot divide by zero.
TypeError: unsupported operand type(s) for /: 'str' and 'int'
Type hints are stored in __annotations__ but not enforced — divide("6", 2) got past the call and the zero check, and only failed at the division itself, deep inside the function. Checking them is the job of a tool like mypy (see type hints).
Practice
1. Write create_profile(name, role="user", active=True) that returns a dict, and call it three ways.
Solution
def create_profile(name, role="user", active=True):
return {"name": name, "role": role, "active": active}
print(create_profile("Alice"))
print(create_profile("Bob", role="admin"))
print(create_profile("Carol", active=False))
$ python profile.py
{'name': 'Alice', 'role': 'user', 'active': True}
{'name': 'Bob', 'role': 'admin', 'active': True}
{'name': 'Carol', 'role': 'user', 'active': False}
Passing active=False by keyword skips over role, which keeps its default.
2. Sort ["elephant", "cat", "dog", "hippopotamus", "ant"] by length, shortest first. Why does "cat" come before "ant"?
Solution
words = ["elephant", "cat", "dog", "hippopotamus", "ant"]
print(sorted(words, key=len))
print(sorted(words, key=lambda w: (len(w), w)))
$ python sort_by_length.py
['cat', 'dog', 'ant', 'elephant', 'hippopotamus']
['ant', 'cat', 'dog', 'elephant', 'hippopotamus']
Python’s sort is stable: words of equal length keep their original order, and cat, dog, ant appear in that order in the input. A tuple key breaks the tie alphabetically.
3. Write add_all(*numbers) that sums any number of arguments, and call it with a list using *.
Solution
def add_all(*numbers):
total = 0
for n in numbers:
total += n
return total
print(add_all(1, 2))
print(add_all(1, 2, 3, 4, 5))
print(add_all(*[10, 20]))
$ python add_all.py
3
15
30
4. Write make_tag(tag, /, text, **attrs) that returns an HTML string like <a href="https://example.com">home</a>, and show it accepts any attribute name.
Solution
def make_tag(tag, /, text, **attrs):
attr_str = "".join(f' {k}="{v}"' for k, v in attrs.items())
return f"<{tag}{attr_str}>{text}</{tag}>"
print(make_tag("a", "home", href="https://example.com"))
print(make_tag("p", text="hi", tag="intro"))
$ python make_tag.py
<a href="https://example.com">home</a>
<p tag="intro">hi</p>
Because tag is positional-only, the keyword tag="intro" doesn’t collide with it and lands in **attrs instead.