"Functionality is the heart of programming, functions are the veins."
- Introduction to Functions
- Parameters and Arguments
- Positional vs Key Arguments
- Scopes
- Return
- Optional Parameters
Args
andKwargs
- Argument Ordering
- Quiz
- Homework
A function in programming is a self-contained, reusable block of code which acts as a mini-program within a larger program.
Functions allow you to segment your code into modular, manageable pieces, thereby enhancing readability, simplifying debugging and improving coding experience overall.
Let's say we have a complex repetitive task, baking cakes. And let's say that in order to bake a singular cake we have to run this code:
print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")
So if we had to bake 4 cakes in different times, and we had to do it without using functions, our code would look like this:
print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")
print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")
print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")
print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")
Let's write the same code, but with functions this time.
In Python functions are defined by def
keyword followed by the name of the function
, then curly brackets ()
and semicolon :
.
NOTE: Take a look at indentation, in case it's wrong the Pyhton
interpreter will not be able to compile the
code.
def print_cake_recipe():
print("1. Preheat the oven to 350°")
print("2. Mix flour, sugar, and eggs.")
print("3. Bake for 30 minutes.")
print("4. Let the cake cool and serve.")
print("\n")
print_cake_recipe() # we call the function in order to execute code inside it
print_cake_recipe()
print_cake_recipe()
print_cake_recipe()
We created a function print_cake_recipe()
and then called it 4 times, lets see what
happens when we run it.
1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.
1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.
1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.
1. Preheat the oven to 350°
2. Mix flour, sugar, and eggs.
3. Bake for 30 minutes.
4. Let the cake cool and serve.
Now you can see that it is a very convenient way to write the programs, as you can collect chunks of your code into functions and call them every time you need.
NOTE: If we try to assign function value to a variable, it will be None
, more on this in "10.5 Return".
Often you need to work with dynamic values within your function and the optimal approach to this challenge would be implementing parameters.
Objective: Write an addition
function that would take two values and print the sum of these two values.
Here is an example of how this code would look like:
def addition(a, b):
print(a + b)
addition(1, 3)
4
Define function addition
and in curly brackets we declared the parameters: a
and b
.
Then we called the function and passed arguments 1
and 3
, as a
and b
accordingly
As irrelevant as it might seem, there is a difference between these two key terms. The function parameters are the names listed in the function's definition and the function arguments are the real values passed to the function.
You just need to understand that once you pass value 11
to the parameter a
into the function, it becomes and argument.
When calling functions in Python, the arguments you pass can be either positional or keyword arguments. Understanding the difference and the proper usage of each type is crucial for writing clear and error-free code.
Positional arguments are arguments that need to be included in the correct order. The order in which you pass the values when calling the function should match the order in which the parameters were defined in the function.
def create_profile(name, age, profession):
print(f"Name: {name}, Age: {age}, Profession: {profession}")
create_profile("Alice", 30, "Engineer")
Name: Alice, Age: 30, Profession: Engineer
In this example, "Alice"
is passed as the name
, 30
as the age
, and "Engineer"
as the profession
, following the order they were defined in the function.
We did it sequentially, so that params are in the same order. Never mix the order of your arguments, it can blow out your code!
Keyword arguments, on the other hand, are arguments passed to a function, accompanied by an identifier.
You explicitly state which parameter you're passing the argument to by using the name of the parameter. This means the order of the arguments does not matter, as long as all required parameters are provided.
def create_profile(name, age, profession):
print(f"Name: {name}, Age: {age}, Profession: {profession}")
create_profile(age=30, profession="Engineer", name="Alice")
Name: Alice, Age: 30, Profession: Engineer
Here, even though the order of arguments is different from the order of parameters in the function definition, Python knows which argument corresponds to which parameter, thanks to the keyword parameters.
Personally I prefer the following approach, following it, your codebase becomes much cleaner.
The scope of a variable refers to the context in which it is visible or accessible in the code. In Python, scopes help manage and isolate variables in different parts of the program, ensuring that variable names don't clash and create unexpected behaviors.
The two main types of scopes are local and global.
Variables declared within a function have local scope, they are created when function is called and destroyed when it finishes its execution. Local variables declared in a functions can only be accessed from within this function.
def addition():
suma = 10 + 15
print(suma)
addition()
25
def addition():
summ = 10 + 15
print(summ)
print(summ)
NameError: name 'summ' is not defined.
In this case we declare variable suma
and since we created it inside the function
it has local scope, therefore we wouldn't be able to call it from outside:
Variables declared outside all the functions have a global scope. They can be accessed from any part of the code, including inside functions, unless overshadowed by a local variable with the same name.
total = 50
def print_added_total():
print(total + 50)
print_added_total()
100
Here, we declared variable outside the function, therefore it has global scope and can be accessed and modified from wherever one might need.
However, if we attempt to reassign a different value to the global variable from within the function the program will not work as intended.
total = 50
def update_total():
total += 50
update_total()
UnboundLocalError: local variable 'total' referenced before assignment
This happens because you can't reassign value to a variable, unless you use keyword global
, however
it is strongly advised not to use it, as this will introduce redundant complexity to your code, making it less clear and
more bug prone.
Here it is important to understand the difference between reassigning a value and modifying an object as if we attempt to append an element to a list declared with global scope it won't cause any problems.
list1 = ["a", "b"]
def update_list():
list1.append("c")
update_list()
print(list1)
['a', 'b', 'c']
-
Use Few Global Variables: Try to use global variables (those outside functions) as little as possible to avoid confusion.
-
Keep Variables Local: Use variables inside functions for things that only matter in that function. This helps keep your code clean and easy to understand.
-
Be Careful with Same Names: If you use the same name for a variable inside and outside a function, the function will only know about the inside one.
The return
statement in a function sends a value from within the function's local scope to where the function has been called. It's a powerful way to pass data out of a function and can be used to send any type of object back to the caller.
The return
statement is followed by the value or expression you want to return. If no value or expression is specified, the function will return None
.
def addition(a, b):
return a + b
sum_of_two_numbers = addition(1, 3)
print(sum_of_two_numbers)
4
In this example, the addition
function takes two positional arguments a
and b
and returns their sum returning the value to sum_of_two_numbers
variable.
Note: Any operation after the return
statement will not be executed.
def addition(a, b):
return a + b
print("this message will never be output")
sum_of_two_numbers = addition(1, 3)
print(sum_of_two_numbers)
4
In Python functions can be called with a varying number of parameters. This feature enhances flexibility and usability of the code depending on the scenario. Optional parameters have default values, which are used if no argument is passed during the function call.
Optional parameters make your functions more flexible. They allow you to create more generalized functions that can handle a wider range of inputs.
Thanks to optional parameters you will be able to use the same function for slightly different purposes without overloading it with arguments or creating multiple, nearly identical functions.
To define an optional parameter, you assign it a default value in the function's definition using operator =
. This default value is used if the caller does not provide a value for that parameter.
def greet(name, message="Hello"):
print(f"{message}, {name}!")
greet("Alice")
greet("Bob", "Good morning")
Hello, Alice!
Good morning, Bob!
In the greet
function above, name
is a mandatory parameter, while message
is optional with a default value of "Hello"
. If no message
is provided when the function is called, it uses the default value.
Important: You can only assign default values to parameters After you have declared your positional arguments, more on this in args/kwargs section
In Python, *args
and **kwargs
are special operators used in function definitions. They allow a function to accept a variable number of arguments, making your functions more flexible. *args
is used for positional arguments, while **kwargs
is used for keyword arguments.
The *args
parameter allows a function to take any number of positional arguments without having to define each one individually.
Note The arguments passed to *args
are accessible as a tuple. Therefore, contents of args
can not and should not be altered in traditional ways.
def calculate_sum(*numbers):
total = sum(numbers)
return total
print(calculate_sum(10, 20, 30)) # Output: 60
In this example, calculate_sum()
can take any number of numerical arguments,
sum them up, and return the total. The *numbers
parameter collects all the
positional arguments into a tuple called numbers
.
As we learned previously in data types, you can use *
to unpack the elements of an iterable, it is relevant as if you
will try to pass an iterable to a function without unpacking it, it will be treated as a whole object.
NOTE: Same applies for **kwargs
list1 = ["a", "b", "c"]
def example(*args):
for argument in args:
print(argument)
example(list1)
['a', 'b', 'c']
Therefore, if you want to pass all the elements of list1
we will need to unpack them first.
list1 = ["a", "b", "c"]
def example(*args):
for argument in args:
print(argument)
example(*list1)
a
b
c
The **kwargs
parameter allows a function to accept any number of keyword arguments. This is useful when you want to handle named arguments in your function. The keyword arguments passed to **kwargs
are stored in a dictionary.
def student_info(**details):
for key, value in details.items():
print(f"{key}: {value}")
student_info(name="John", grade="A", subject="Mathematics")
name: John
grade: A
subject: Mathematics
In this example, student_info()
can accept any number of keyword arguments. The **details
parameter collects all the keyword arguments into a dictionary called details
.
When defining a function, it's important to follow the correct order of parameters to avoid syntax errors. The order should be:
- Standard arguments
*args
**kwargs
This order ensures that your function can handle a mix of standard, positional, and keyword arguments effectively.
def mix_and_match(a, b, *args, **kwargs):
pass # Function implementation
Incorrect Syntax - Will Cause an Error!
def mix_and_match(a, b, **kwargs, *args):
pass
SyntaxError: invalid syntax
What will the output be for the following function call?
def greet(name):
return "Hello, " + name
print(greet("Alice"))
A) Hello, Alice
B) Hello,
C) Alice
D) It will raise an error.
Given this coffee-making function, what will the following function call output?
def make_coffee(size="Medium", type="Cappuccino"):
return f"Making a {size} {type} coffee."
print(make_coffee("Large"))
A) Making a Large Cappuccino coffee.
B) Making a Large coffee.
C) Making a Medium Cappuccino coffee.
D) It will raise an error.
What does the
*args
parameter in a function allow you to do?
A) It allows the function to accept any number of keyword arguments.
B) It allows the function to accept a list of arguments.
C) It allows the function to accept any number of positional arguments.
D) It unpacks the arguments passed to the function.
What will be the output of the following code?
def user_profile(**details):
return details.get("name", "Anonymous") + " - " + details.get("role", "Guest")
print(user_profile(name="John", age=30, role="Admin"))
A) John - 30
B) John - Admin
C) Anonymous - Guest
D) It will raise an error.
Consider this function. What is true about the
return
statement in this function?
def add_numbers(a, b):
result = a + b
return result
print("Calculation has been completed")
A) It outputs the result of the function.
B) It stops the function's execution and returns the result.
C) It prints the result before ending the function.
D) It is optional and can be omitted.
Given this code, what will the output be?
def calculate_difference(a, b):
result = a - b
return result
calculate_difference(10, 5)
print(result)
A) 5
B) 10
C) result
D) It will raise a NameError
.
Consider the function and its call below. What will the output be?
def display_info(name, age):
return f"Name: {name}, Age: {age}"
print(display_info(age=25, name="Emma"))
A) Name: Emma, Age: 25
B) Name: 25, Age: Emma
C) Name: name, Age: age
D) It will raise an error.
In the function definition below, which parameters are considered optional?
def func(a, b=5, c=10):
return
A) Only a
B) Both b
and c
C) Only b
D) All a
, b
, and c
Which of the following is the correct way to define a function with all types of arguments?
A) def func(*args, a, b, **kwargs):
B) def func(a, *args, b, **kwargs):
C) def func(a, b, *args, **kwargs):
D) def func(**kwargs, *args, a, b):
Given the function and call below, what will the function return?
def multiply_numbers(*args):
result = 1
for number in args:
result *= number
return result
print(multiply_numbers(2, 3, 4))
A) 24
B) 9
C) 6
D) It will raise an error.
What will be the output of the following code?
x = 10
def print_number():
x = 5
print("Inside function:", x)
print_number()
print("Outside function:", x)
A) Inside function: 5, Outside function: 5
B) Inside function: 10, Outside function: 10
C) Inside function: 5, Outside function: 10
D) It will raise a NameError
.
Objective: Implement a function named draw_rectangle
that outputs a rectangle made of asterisks (*
). The rectangle should have a width of 7 characters and a height of 6 lines.
- Use nested
for
loops to generate the rectangle. - The outer loop should iterate through the lines (height), and the inner loop should iterate through the characters (width) on each line.
- Only the border of the rectangle should be drawn with asterisks, while spaces (
- The function does not need to return anything; it should directly print the rectangle to the console.
*******
* *
* *
* *
* *
*******
Objective: Create a function print_digit_sum
that calculates and prints the sum of all digits in a given integer.
- The function should accept a single integer argument, possibly negative.
- Convert the integer to its absolute value to handle negative numbers.
- Iterate over each digit in the number and calculate the total sum.
- Print the result to the console. The function returns
None
.
print_digit_sum(1234) # Output: 10
print_digit_sum(-567) # Output: 18
Objective: Develop a function get_factors
that returns a list of all the divisors of a given natural number.
- The function should accept a single integer argument,
num
. - If
num
is less than 1, return an empty list to reflect the definition of natural numbers. - Efficiently find and return a list of all divisors of
num
. - Ensure correct functionality for both small and large values of
num
.
print(get_factors(28)) # Output: [1, 2, 4, 7, 14, 28]
print(get_factors(13)) # Output: [1, 13]
print(get_factors(0)) # Output: []
Objective: Enhance the convert_temperature
function to support optional parameters for conversion direction.
- The function should accept one mandatory parameter for the temperature value and one optional parameter for the direction of conversion (
'C'
for Celsius to Fahrenheit,'F'
for Fahrenheit to Celsius). - Use default arguments to assume conversion from Celsius to Fahrenheit if the direction is not specified.
- Calculate and return the converted temperature value.
print(convert_temperature(100)) # Assumes Celsius to Fahrenheit, Output: 212
print(convert_temperature(212, convert_to='C')) # Fahrenheit to Celsius, Output: 100
Objective: Implement a function calculate_statistics
that computes various statistical measures (mean, median, mode, range) for a dataset, based on specified options.
- The function should accept an arbitrary number of positional arguments (
*data
) representing the dataset. - Accept keyword arguments (
**options
) to specify which statistics to calculate:mean
,median
,mode
,range
. If an option isTrue
, calculate that statistic. - Return a dictionary with keys as the names of the statistics calculated and their corresponding results as values.
- If no options are specified, calculate and return all statistics.
- Handle edge cases such as empty datasets or datasets without a mode.
data_points = [4, 1, 2, 2, 3, 5]
print(calculate_statistics(*data_points, mean=True, range=True))
# Output: {'mean': 2.8333333333333335, 'range': 4}
print(calculate_statistics(*data_points))
# Output: {'mean': ..., 'median': ..., 'mode': ..., 'range': ...}