📘 Day 17: Writing your first function (def, parameters, return)
def is the keyword that tells Python you are creating a function – a reusable block of code that can take parameters (inputs), do something, and optionally return a result.
Why use functions?
- Modularity – break a large program into smaller, manageable pieces.
- Reusability – call the same logic many times without rewriting it.
- Readability – give a meaningful name to a task, so the code reads like plain English.
When you write a function, three things happen behind the scenes:
1. Definition time – Python stores the function object under the name you give it. No code inside runs yet.
2. Call time – you use the name followed by parentheses, optionally passing arguments that match the parameters. Python creates a new local scope for that call.
3. Return – the return statement sends a value back to the caller and ends the function immediately. If you omit return, Python automatically returns None.
Step‑by‑step example
What each part does
-
- The triple‑quoted string right after the header is a docstring. It describes what the function does and can be accessed via
-
-
-
-
-
Key points to remember
- Parameters vs. arguments – Parameters are the names in the function definition; arguments are the actual values you pass when calling.
- Scope – Variables created inside a function are local and disappear after the function returns. They do not affect variables with the same name outside.
- Return value – Only one
- No return – If a function reaches the end without a
A slightly more complex function
(continued below 👇)
def is the keyword that tells Python you are creating a function – a reusable block of code that can take parameters (inputs), do something, and optionally return a result.
Why use functions?
- Modularity – break a large program into smaller, manageable pieces.
- Reusability – call the same logic many times without rewriting it.
- Readability – give a meaningful name to a task, so the code reads like plain English.
When you write a function, three things happen behind the scenes:
1. Definition time – Python stores the function object under the name you give it. No code inside runs yet.
2. Call time – you use the name followed by parentheses, optionally passing arguments that match the parameters. Python creates a new local scope for that call.
3. Return – the return statement sends a value back to the caller and ends the function immediately. If you omit return, Python automatically returns None.
Step‑by‑step example
def greet(name):
"""
Build a friendly greeting.
The parameter `name` receives the caller's argument.
"""
# Inside the function we create a local variable `message`.
message = f"Hello, {name}!" # f‑string inserts the value of `name`.
return message # Send `message` back to the caller.
# ---- calling the function ----
user_input = "Alice" # This could come from anywhere.
result = greet(user_input) # `user_input` is passed as an argument.
print(result) # Prints: Hello, Alice!
What each part does
-
def greet(name): – Starts the definition. greet is the function name; name is a parameter that will receive a value when we call it. - The triple‑quoted string right after the header is a docstring. It describes what the function does and can be accessed via
greet.__doc__. -
message = f"Hello, {name}!" – A local variable; it exists only while this call runs. The f‑string formats the greeting by inserting the current value of name. -
return message – Sends the constructed string back to wherever the function was called. Execution of the function stops here. -
user_input = "Alice" – An ordinary variable holding the data we want to greet. -
result = greet(user_input) – Calling the function. user_input is an argument that gets bound to the parameter name. The function runs, returns the greeting, and that value is stored in result. -
print(result) – Uses the returned value just like any other variable. Key points to remember
- Parameters vs. arguments – Parameters are the names in the function definition; arguments are the actual values you pass when calling.
- Scope – Variables created inside a function are local and disappear after the function returns. They do not affect variables with the same name outside.
- Return value – Only one
return is executed per call. If you need to exit early, you can place return statements in conditional blocks. - No return – If a function reaches the end without a
return, Python automatically returns None, which is a special “nothing” object. A slightly more complex function
def calculate_stats(numbers):
"""Return the sum and average of a list of numbers."""
total = sum(numbers) # built‑in sum adds all items
average = total / len(numbers) # len gives the count
return total, average # returns a tuple (total, average)
data = [4, 8, 15, 16, 23, 42]
sum_val, avg_val = calculate_stats(data)
print(f"Sum: {sum_val}, Average: {avg_val:.2f}")
(continued below 👇)
- Here
- The
By mastering def, parameters, and return, you now have the core tools to build any reusable piece of logic in Python. Practice by writing small functions that perform arithmetic, string manipulation, or data processing, and you’ll quickly see how they make your code cleaner and more powerful.
calculate_stats takes one parameter numbers, which we expect to be an iterable of numeric values. - The
return total, average line actually returns a tuple containing both results. When we call the function, we can unpack that tuple directly into sum_val and avg_val. By mastering def, parameters, and return, you now have the core tools to build any reusable piece of logic in Python. Practice by writing small functions that perform arithmetic, string manipulation, or data processing, and you’ll quickly see how they make your code cleaner and more powerful.
What does a Python function return if it reaches the end without a `return` statement?
Anonymous Quiz
0%
The last evaluated expression
0%
None
0%
0
0%
An empty string
📘 Day 18: Default arguments and keyword arguments
Default arguments let you give a function parameter a value that will be used if the caller does not supply one. Keyword arguments let the caller specify a value by naming the parameter, so the order of the arguments can change.
When Python creates a function object, it stores the default values in a tuple called
How the interpreter matches arguments
1. Positional arguments are matched to parameters in the order they appear.
2. Keyword arguments are matched by name, regardless of position.
3. After all positional arguments are assigned, any remaining parameters that have defaults get those defaults automatically.
4. If a parameter has no default and the caller didn’t provide a value, Python raises
Why keyword arguments are powerful
- You can skip over optional parameters you don’t want to change.
- The call becomes self‑documenting:
- They allow you to call a function with a subset of optional arguments without remembering the exact order.
Common pitfalls with mutable defaults
Calling this repeatedly:
What happened?
The list
Correct pattern – use
Now the calls are independent:
Full runnable example – mixing defaults and keywords
Walk‑through
-
- The function body builds a formatted string using the three parameters.
- In call 1, only
- In call 2,
- In call 3, every argument is supplied by name, so the order can be shuffled.
- In call 4,
Quick checklist (bolded list)
(continued below 👇)
Default arguments let you give a function parameter a value that will be used if the caller does not supply one. Keyword arguments let the caller specify a value by naming the parameter, so the order of the arguments can change.
When Python creates a function object, it stores the default values in a tuple called
__defaults__. This tuple is built once, at function definition time, not each time the function runs. Because of that, the same object is reused for every call that omits the argument. This is why mutable defaults (like a list or dict) can keep state between calls – the object lives as long as the function object lives.How the interpreter matches arguments
1. Positional arguments are matched to parameters in the order they appear.
2. Keyword arguments are matched by name, regardless of position.
3. After all positional arguments are assigned, any remaining parameters that have defaults get those defaults automatically.
4. If a parameter has no default and the caller didn’t provide a value, Python raises
TypeError.Why keyword arguments are powerful
- You can skip over optional parameters you don’t want to change.
- The call becomes self‑documenting:
process(data, mode='fast') tells the reader what 'fast' means.- They allow you to call a function with a subset of optional arguments without remembering the exact order.
Common pitfalls with mutable defaults
def append_to_log(message, log=[]):
"""Add a message to a growing log list."""
log.append(message)
return log
Calling this repeatedly:
print(append_to_log('first')) # ['first']
print(append_to_log('second')) # ['first', 'second']What happened?
The list
log is created once when append_to_log is defined. Each call re‑uses the same list, so it accumulates items across calls. This is often unintended.Correct pattern – use
None as the sentinel and create a new list inside the function:def append_to_log(message, log=None):
"""Add a message to a fresh log list each call."""
if log is None:
log = [] # new list for this call
log.append(message)
return log
Now the calls are independent:
print(append_to_log('first')) # ['first']
print(append_to_log('second')) # ['second']Full runnable example – mixing defaults and keywords
def greet(name, greeting='Hello', punctuation='!'):
"""
Return a personalised greeting.
* name – required, positional or keyword
* greeting – optional, defaults to 'Hello'
* punctuation – optional, defaults to '!'
"""
return f"{greeting}, {name}{punctuation}"
# 1️⃣ Positional only
print(greet('Alice')) # Hello, Alice!
# 2️⃣ Positional + one keyword (out of order)
print(greet('Bob', punctuation='.' )) # Hello, Bob.
# 3️⃣ All keywords – order doesn't matter
print(greet(name='Carol', punctuation='?', greeting='Hi')) # Hi, Carol?
# 4️⃣ Mixing positional and keyword
print(greet('Dave', greeting='Good morning')) # Good morning, Dave!
Walk‑through
-
def greet(name, greeting='Hello', punctuation='!'): defines two default arguments (greeting and punctuation). - The function body builds a formatted string using the three parameters.
- In call 1, only
name is supplied; the defaults fill in greeting and punctuation.- In call 2,
name and punctuation are given; greeting falls back to its default.- In call 3, every argument is supplied by name, so the order can be shuffled.
- In call 4,
name is positional, greeting is a keyword, and punctuation uses its default.Quick checklist (bolded list)
(continued below 👇)
- Always place parameters with defaults after those without.
- Use
- Keyword arguments can be mixed with positional but must come after all positional arguments in the call.
- Defaults are evaluated once at definition time, not at each call.
- Use
None as a sentinel for mutable default values. - Keyword arguments can be mixed with positional but must come after all positional arguments in the call.
- Defaults are evaluated once at definition time, not at each call.
Which statement about default argument values in Python is true?
Anonymous Quiz
100%
They are evaluated each time the function is called
0%
They are evaluated once when the function is defined
0%
They can only be immutable objects
0%
They must be literals
📘 **Day 19: *args and **kwargs**
When you define a Python function, you usually list every parameter it expects:
```python
def greet(name, age):
print(f"Hello {name}, you are {age} years old.")
```
But real‑world functions often need **flexibility** – they may receive an unknown number of values or a set of named options that can change from call to call. That’s where `*args` and `**kwargs` come in.
**What `*args` does**
- The asterisk (`*`) before a parameter name tells Python *“collect any extra *positional* arguments into a tuple”*.
- Inside the function, `args` behaves exactly like any other tuple: you can iterate, index, or unpack it.
**What `**kwargs` does**
- The double asterisk (`**`) before a parameter name tells Python *“collect any extra *keyword* arguments into a dictionary”*.
- Inside the function, `kwargs` works like any dict: you can query keys, iterate over items, or use `.get()`.
**Why it works**
When a function call is evaluated, Python separates arguments into three groups:
1. **Positional arguments** that match the function’s regular parameters.
2. **Extra positional arguments** that don’t have a matching parameter – they go into `*args`.
3. **Keyword arguments** that match named parameters, plus any additional named arguments – the extras go into `**kwargs`.
Python builds `args` as a **tuple** because the order of positional arguments matters and they are immutable. It builds `kwargs` as a **dict** because each keyword must be unique and you need fast lookup by name.
**Calling a function with `*` and `**`**
The same symbols can be used *in reverse* when you **unpack** a sequence or mapping into a function call:
```python
values = (1, 2, 3)
options = {"verbose": True, "mode": "fast"}
def demo(a, b, c, verbose=False, mode="normal"):
print(a, b, c, verbose, mode)
demo(*values, **options) # expands to demo(1, 2, 3, verbose=True, mode="fast")
```
**Ordering rules**
When you mix regular parameters, `*args`, and `**kwargs` in a definition, the order must be:
```
def func(pos1, pos2, *args, kw1=None, kw2=None, **kwargs):
...
```
- Positional‑only (if using `/`), then normal positional, then `*args`, then keyword‑only, then `**kwargs`.
---
**Complete runnable example**
```python
def summarize(*numbers, **options):
"""
Prints a summary of any numeric values passed in,
and shows optional settings provided as keywords.
"""
# 1️⃣ *numbers is a tuple of all extra positional arguments
total = sum(numbers)
count = len(numbers)
# 2️⃣ **options is a dict of all extra keyword arguments
prefix = options.get("prefix", "Result")
show_average = options.get("average", False)
print(f"{prefix}:")
print(f" Count = {count}")
print(f" Sum = {total}")
if show_average and count > 0:
print(f" Avg = {total / count}")
# Calling with various combos
summarize(4, 7, 1, 3, prefix="Stats", average=True)
summarize(10, 20) # no extra keywords
summarize() # no numbers at all
```
**Walk‑through**
1. `def summarize(*numbers, **options):` –
- `*numbers` gathers **any number** of positional arguments into a tuple called `numbers`.
- `**options` gathers **any number** of keyword arguments into a dict called `options`.
2. Inside the function:
- `total = sum(numbers)` uses the built‑in `sum` on the tuple – works because tuples are iterable.
- `count = len(numbers)` gives how many numbers were passed.
- `options.get("prefix", "Result")` fetches the value for key `"prefix"` if present; otherwise defaults to `"Result"`.
- `options.get("average", False)` decides whether to display the average.
(continued below 👇)
When you define a Python function, you usually list every parameter it expects:
```python
def greet(name, age):
print(f"Hello {name}, you are {age} years old.")
```
But real‑world functions often need **flexibility** – they may receive an unknown number of values or a set of named options that can change from call to call. That’s where `*args` and `**kwargs` come in.
**What `*args` does**
- The asterisk (`*`) before a parameter name tells Python *“collect any extra *positional* arguments into a tuple”*.
- Inside the function, `args` behaves exactly like any other tuple: you can iterate, index, or unpack it.
**What `**kwargs` does**
- The double asterisk (`**`) before a parameter name tells Python *“collect any extra *keyword* arguments into a dictionary”*.
- Inside the function, `kwargs` works like any dict: you can query keys, iterate over items, or use `.get()`.
**Why it works**
When a function call is evaluated, Python separates arguments into three groups:
1. **Positional arguments** that match the function’s regular parameters.
2. **Extra positional arguments** that don’t have a matching parameter – they go into `*args`.
3. **Keyword arguments** that match named parameters, plus any additional named arguments – the extras go into `**kwargs`.
Python builds `args` as a **tuple** because the order of positional arguments matters and they are immutable. It builds `kwargs` as a **dict** because each keyword must be unique and you need fast lookup by name.
**Calling a function with `*` and `**`**
The same symbols can be used *in reverse* when you **unpack** a sequence or mapping into a function call:
```python
values = (1, 2, 3)
options = {"verbose": True, "mode": "fast"}
def demo(a, b, c, verbose=False, mode="normal"):
print(a, b, c, verbose, mode)
demo(*values, **options) # expands to demo(1, 2, 3, verbose=True, mode="fast")
```
**Ordering rules**
When you mix regular parameters, `*args`, and `**kwargs` in a definition, the order must be:
```
def func(pos1, pos2, *args, kw1=None, kw2=None, **kwargs):
...
```
- Positional‑only (if using `/`), then normal positional, then `*args`, then keyword‑only, then `**kwargs`.
---
**Complete runnable example**
```python
def summarize(*numbers, **options):
"""
Prints a summary of any numeric values passed in,
and shows optional settings provided as keywords.
"""
# 1️⃣ *numbers is a tuple of all extra positional arguments
total = sum(numbers)
count = len(numbers)
# 2️⃣ **options is a dict of all extra keyword arguments
prefix = options.get("prefix", "Result")
show_average = options.get("average", False)
print(f"{prefix}:")
print(f" Count = {count}")
print(f" Sum = {total}")
if show_average and count > 0:
print(f" Avg = {total / count}")
# Calling with various combos
summarize(4, 7, 1, 3, prefix="Stats", average=True)
summarize(10, 20) # no extra keywords
summarize() # no numbers at all
```
**Walk‑through**
1. `def summarize(*numbers, **options):` –
- `*numbers` gathers **any number** of positional arguments into a tuple called `numbers`.
- `**options` gathers **any number** of keyword arguments into a dict called `options`.
2. Inside the function:
- `total = sum(numbers)` uses the built‑in `sum` on the tuple – works because tuples are iterable.
- `count = len(numbers)` gives how many numbers were passed.
- `options.get("prefix", "Result")` fetches the value for key `"prefix"` if present; otherwise defaults to `"Result"`.
- `options.get("average", False)` decides whether to display the average.
(continued below 👇)
3. The three calls demonstrate flexibility:
- The first call passes four numbers and two keywords (
- The second call passes only numbers, relying on default keyword values.
- The third call passes nothing;
Key take‑aways
-
-
- The same
- Using these constructs lets you write highly reusable and extensible functions without fixing the exact number or names of arguments up front.
- The first call passes four numbers and two keywords (
prefix and average). - The second call passes only numbers, relying on default keyword values.
- The third call passes nothing;
numbers becomes an empty tuple, options an empty dict, and the function still runs safely.Key take‑aways
-
*args packs extra positional arguments into a tuple. -
**kwargs packs extra keyword arguments into a dictionary. - The same
* and ** symbols unpack a sequence or mapping when calling a function. - Using these constructs lets you write highly reusable and extensible functions without fixing the exact number or names of arguments up front.
What type does Python store extra positional arguments in when using *args?
Anonymous Quiz
0%
list
100%
tuple
0%
set
0%
dict