Session 5: List Comprehension and Python Functions
Date: 04-04-2026 4:00 pm to 6:00 pm**
Agenda
- Review of previous sessions
- Answers to queries
- List comprehensions
- Unpacking rules
- Function parameters and arguments revisited
List comprehensions
There are several ways to create a list:
>>> a = [1, 2, 3, 4, 5] # Assign a list to an object
>>> a
[1, 2, 3, 4, 5]
>>> b = list([1, 2, 3, 4, 5]) # Use list() with a list argument
>>> b
[1, 2, 3, 4, 5]
>>> c = list(range(1, 6)) # Use list() with a range() argument
>>> c
[1, 2, 3, 4, 5]
Another way is to start with an empty list and append elements one at a time.
Python provides a short-hand notation to achieve this: This is how it works:- The
forloop iterates overrange(1, 6), namely, generates the values1, 2, 3, 4, 5 - The iterator
iis assigned these values in sequence, one at a time - The value of the iterator
iis appended to the list being generated
Instead of appending the value generated by the range(1, 6) generator, if we wished to store not i but its square i * i, the list comprehension would be as follows:
i * i as compared to i the previous time.
Consider another example that applies a condition to select elements to be added to the list. For exampe, if only the elements that satisfy a given condition are to be appended to the list:
>>> a = [i * i for i in range(10)]
>>> a
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> b = [i * i for i in range(10) if i * i % 2]
>>> b
[1, 9, 25, 49, 81]
i * i % 2 tests the condition that remainder of i * i % 2 is True (that is non-zero). Therefore 0, 4, 15, 36, 64 are not appended because they result in i * i % 2 == 0.
An if-else condition can be incorporated into a list comprehension. In the following example i * i is appended if i * i % 2 is True and i is appended if i * i % 2 is False:
Unpacking rules
Elements of a container can be unpacked into objects:
It is possible to ignore one or more of the returned values by assigining the underscore (_) in place of the name of an object.
>>> x, _, z = [10, "Hello", 20]
>>> print(x, z)
10 20
>>> m, _, _ = [10, "Hello", 20]
>>> print(m)
10
Note
_ is a valid variable name. By conevntion, it is used when values that are not intended to be used subsequently. It is a convenient shortcut, instead of using names such as dummy or other similar names to indicate it is being ignored.
Warning
Specifying fewer objects on the left side than the number of elements on the right side raises an error:
With two values on the left side, the expected number of values on the right side expected to be unpacked is two, but it turns out to be three. That raises the error.Python offers a way to unpack where an object on the left side can collect zero or more values.
>>> a, *b = [10, "Hello", 20]
>>> type(a), a
(<class 'int'>, 10)
>>> type(b), b
(<class 'list'>, ['Hello', 20])
b is of type list and can contain zero or more elements, and in this specific case, contains two objects.
It is possible to use this technique in the following ways:
Note
The _ variable can be used to catch multiple values by preceding it with a *.
Note
- When unpacked, the right side will result in four elements.
- The first element is assigned to
a. - The last element is assigned to
c. - The remaining two elements inbetween the first and the last are assigned to
b, which is alistand can hold zero or more elements. Even when there is only one element assigned tob, it is alistwith one element, not a single non-container object. bcan be an empty list. Since the values on the right side are unpacked to the objects on the left and none are left over to assign tob.- When the objects on the left are more than the values on the right, an exception is raised.
Note
A tuple can be unpacked exactly similar to unpacking a list. You can replace a list in any of the above examples with a tuple and things work as yu would expect.
When you unpack more than one element from a tuple to a variable on the left side (using the *), the type of the variable on the left side is a list and not a tuple.
Function parameters and arguments revisited
The previous discussion on function interface, parameters and arguments can be summarized as follows:
Note
A function defines a set of parameters — names that act as placeholders for values the function needs. When you call the function, you supply arguments, which are the actual values passed into those parameters. Python then performs a process called argument binding, matching each argument to the correct parameter based on position
This offers the following advantages:
- The design of a function can be done independent of how the function will be called later and what the names of arguments are going to be named.
- The body of the function, which may contain any number of lines and implement any complex logic, can now be represented by a single function call.
- Variables used within the body of the function and the parameters are local to the function. Therefore these names can be used anywhere outside the function without clashing or overwriting these names.
A function achieves two important goals (See Think Python, Allen Downey 3ed.):
- Wrapping a piece of code up in a function is called encapsulation. One of the benefits of encapsulation is that it attaches a name to the code, which serves as a kind of documentation. Another advantage is that if you re-use the code, it is more concise to call a function twice than to copy and paste the body!
- Adding a parameter to a function is called generalization
By default, a function must be called with the correct number of arguments. Exception to the rule is when the function has default parameters. In that case, you can provide fewer arguments than there are parameters, and the missing arguments are assigned the values specified for the default parameter. For this reason, all the default parameters (if a function has any) must be defined at he end of the parameter list. It is not permitted to define parameters without default values after a parameter has been assigned a default value.
Therefore the following function signature results in an error:
>>> def func(a = 10, b):
File "<python-input-5>", line 1
def func(a = 10, b):
^
SyntaxError: parameter without a default follows parameter with a default
If such a function could be defined and is called with one argument, say func(20), then it results in an ambiguous situation where it is not possible to decide whether it is to be interpreted as
a = 20andbis missing
or
a = 10, the default value andb = 20
Warning
If a function requires default parameters, they must appear after all the non-default parameters.
Calling functions with named arguments
def echo_lines(line: str, n: int) -> str:
if n < 0:
raise ValueError(f"n = {n} must not be negative")
s = ""
for i in range(n):
s += f"{line}\n"
return s
print(echo_lines("Hello, world!", 2))
print(echo_lines(n=2, line="Hello, world!"))
- When arguments are passed without names, they are assigned to the parameters based on their position. The first argument is assigned to the first parameter and the second argument to the second parameter.
- Python allows naming arguments when calling a function and in such a case, the order of the arguments can be arbitrary. The names must be the same as the names of the parameters.
This works with default parameters as well. If a parameter with a default value is missing from the arguments, it is assigned the default value for that parameter during the specific function call.
Defining functions with a variable number of positional arguments
Consider the built-in function print() which prints the values of the arguments furnished when it is called, and this is not a fixed number. Without any arguments, print() prints an empty line, and when called with one, or two or any number of arguments, it prints all the arguments, and this number can be different in another onvocation of the function.
Python has a mechanism to define a function with a variable number of arguments, which enables a user-defined functin to accept a variable number of arguments.
>>> def func(name: str, *args):
... print(type(name), name)
... print(type(args), len(args), args)
>>> func('Satish', 10, "Hubli")
<class 'str'> Satish
<class 'tuple'> 2 (10, 'Hubli')
>>> func('Satish', 10, "Hubli", "Engineer")
<class 'str'> Satish
<class 'tuple'> 3 (10, 'Hubli', 'Engineer')
>>> func('Satish')
<class 'str'> Satish
<class 'tuple'> 0 ()
args is in fact a tuple and therefore can contain zero or more elements. It is prefixed with a * which implies that that all extra positional arguments must be collected into a single tuple.
The values of the elements of the tuple can be copied into local variables within the function for subsequent use. The programmer can choose which of the elements to choose and which to ignore using the unpacking rules described above. With the number of elements can be known using len(args), it is possible to decide the way the elements of args are to be used.
Defining functions with a variable number of named arguments
Consider the following lines of code:
>>> def func(a, **kwargs):
... print(type(a), a)
... print(type(kwargs), len(kwargs), kwargs)
...
>>> func(10, line="Hello", n=2)
<class 'int'> 10
<class 'dict'> 2 {'line': 'Hello', 'n': 2}
kwargs is in fact a dict and therefore can contain zero or more elements. It is prefixed with a ** which implies that that all extra named arguments must be collected into a single dict.
The values of the elements of the dict can be copied into local variables within the function for subsequent use. The programmer can choose which of the elements to choose and which to ignore using the keys of the dict. With the number of elements can be known using len(args), it is possible to decide the way the elements of kwargs are to be used.
Note
-
The name of the variable positional parameter can be anything chosen by the programmer. It is a convention to name it
args. In the function definition it must be preceded by*and is of typetuple. -
The name of the variable named parameter can be anything chosen by the programmer. It is a convention to name it
kwargs. In the function definition it must be preceded by**and is of typedict.
There are some rules on how the various parameters must be ordered in the function definition:
- Positional-only parameters (x, /)
- Positional-or-keyword parameters (a, b)
- Default-valued parameters (c=10, d=20)
- Variable positional parameters (*args)
- Keyword-only parameters (e, f)
- Keyword-only defaults (g=30)
- Variable keyword parameters (**kwargs)
This can be a bit flustering in the beginning, but will begin to make sense as the programmer gains experience.
Warning
Use *args and/or **kwargs only when absolutely necessary. One situation where they must be used is when one of the parameters to the function is a user defined function and the rest of the parameters are the arguments to the first parameter to be used within the body of the function.
Consider a function that solves a nonlinear equation by the bisection method, and the equation to be solved is defined as a user-defined function with its own set of input arguments. Both the name of the name of the user-defined function and its arguments become input to the function implementing the bisection method, and it is not known at the time of its implementation how many arguments the user-defined function may need and what their names are expected to be.