Previously we learned to control program flow using loops and conditional statements. If you haven't gone through it yet, follow the link below. 👇👇👇
Let's get back for a moment to the sample PyCharm Python script that started our journey. It is stripped from comments, leaving only pure interpretable code.
def print_hi(name):
print(f'Hi, {name}')
if __name__ == '__main__':
print_hi('PyCharm')
If we take a look carefully, we can notice that we have been using functions since day one! print_hi(...) is a function defined by us and print(...) is Python's built-in function. Conversion between types is actually a function call. Since the functions are commonly present, it's worth taking some time to understand them.
What functions are?
Functions are modular blocks of code designed to perform specific tasks. They encapsulate a set of instructions under a meaningful name, making the code more readable, reusable, and manageable. They promote code organization and reduce redundancy, allowing developers to efficiently build and maintain complex programs. By utilizing functions, programs can be broken down into smaller units, enhancing code structure and facilitating collaboration.
Here’s the basic syntax of defining a function in Python.
def function_name(parameter):
# Function body
return result # Optionally, a function can return a value
Let's break it down into components:
def
This keyword signals the start of a function definition.function_name
This is the name given to the function. Choose a descriptive name that reflects the purpose of the function.parameter
This is the input value that the function accepts. It goes inside the parentheses and can be zero or more. If the function doesn’t need any parameters, the parentheses remain empty.:
The colon marks the beginning of the function body.Function body
This is where we write the code that the function will execute when called. It performs the intended task using the provided parameters.return
If the function needs to produce an output, the return statement followed by an expression specifies the value to be returned. If the function doesn’t return anything, we can omit the return statement or use it without an expression to return from a function earlier.
To execute a function, we simply call it by its name and provide optional parameters, and store the return value if any. Let's see an example.
returnedValue = function_name(aParameter)
Now we are ready to write the simplest function.
def print_hello():
print('Hello world!')
print_hello()
print_hello(...) function doesn't take any parameters and doesn't provide any returned value. In its body, it calls Python's built-in print(...) function and passes "Hello world!" string.
❯ python3 main.py
Hello world!
"Hello world!" message is printed on the terminal!
Returning outcome from a function
Usually, functions are useful to perform some operation and provide a result to the caller. To make things a bit more complicated, let's define a function that computes a sum of two numbers.
def add_numbers(a, b):
sum_result = a + b
return sum_result
result = add_numbers(5, 3)
print(result)
add_numbers(...) function takes two arguments, a
and b
, it sums them up and stores the result in sum_result
variable. Then the variable is returned providing the outcome to the caller.
As a function can take multiple parameters, it also can produce multiple outcomes.
def add_and_subtract_numbers(a, b):
sum_result = a + b
diff_result = a - b
return sum_result, diff_result
sum_result, diff_result = add_and_subtract_numbers(10, 5)
print("Sum:", sum_result)
print("Difference:", diff_result)
In this case, the add_and_subtract_numbers(...) function returns two values, which are unpacked into the sum_result
and diff_result
variables.
💡 Unpacking in Python refers to the process of extracting elements from data structures like tuples, lists, or dictionaries, and assigning them to individual variables. This allows us to easily access the individual elements within a collection without needing to access them using indexing or iteration.
Sometimes functions return more than one value, but if we are not interested in all of them, we may want to discard a few.
def add_and_subtract_numbers(a, b):
sum_result = a + b
diff_result = a - b
return sum_result, diff_result
_, diff_result = add_and_subtract_numbers(10, 5)
print("Difference:", diff_result)
The above example shows that we are not interested in sum_result
. To ignore that returned value, we can use underscore.
💡 Discarding a variable using an underscore (_) is a common practice in Python when we receive a value that we don’t intend to use. By assigning the value to an underscore, we indicate that the value is intentionally being ignored or discarded.
Default and named parameters
Default parameters, also known as default arguments or default parameter values, are values assigned to function parameters in advance. These default values are used when a caller of the function does not provide a value for the corresponding parameter. This feature is particularly useful when we want a parameter to have a default behavior but still allow callers to customize it if needed.
In Python, we can define default parameters when declaring a function by assigning a value to the parameter in the function’s parameter list, i.e. parameter_name=value
. When the function is called, if an argument is not provided for that parameter, the default value will be used.
Let's customize our print_hi(...) function.
def print_hi(name, message='Hi'):
print(f'{message}, {name}')
if __name__ == '__main__':
print_hi('PyCharm')
The function now takes two arguments, but the second one is defaulted. We can still call the function providing a single parameter.
❯ python3 main.py
Hi, PyCharm
Now we can provide the second parameter, overriding the default one.
def print_hi(name, message='Hi'):
print(f'{message}, {name}')
if __name__ == '__main__':
print_hi('PyCharm', 'Hello')
We are providing "Hello" as the second parameter.
❯ python3 main.py
Hello, PyCharm
The output has changed, and now the "Hello, PyCharm" message is printed on the terminal.
When a function has several parameters, named parameters, also known as keyword arguments are especially useful if some of them have default values. It is a feature in programming languages that allow us to pass arguments to a function using the names of the parameters rather than relying on their positions. This provides more clarity and flexibility when calling functions, as we can explicitly specify which parameter each argument corresponds to, regardless of the order.
In Python, you can use named parameters when calling functions by using the syntax parameter_name=value
. This allows us to pass arguments to the function in any order, making the code more readable and reducing the chances of making mistakes due to argument ordering.
def print_hi(name, message='Hi'):
print(f'{message}, {name}')
if __name__ == '__main__':
print_hi(message='Hello', name='PyCharm')
In this example, we’ve called the print_hi(...) function with named parameters, explicitly specifying the values for the message
and name
parameters. The order in which the arguments are passed doesn’t matter because the names are used to match the values to the correct parameters.
Named parameters are especially useful when a function has several parameters, some of which have default values. We can choose to only provide values for the parameters we want to customize, and the rest will use their default values.
Inner functions
Inner functions, also known as nested functions, are functions defined inside another function in Python. These inner functions are encapsulated within the scope of the outer function and can only be accessed and used within that scope. This concept of nesting functions allows for more modular and organized code, as well as the ability to create helper functions that are closely related to the main function.
def print_hi_outer(message):
def print_hi_inner(name):
print(f'{message}, {name}')
return print_hi_inner
if __name__ == '__main__':
closure = print_hi_outer('Hi')
closure('Robert')
closure('Paul')
closure = print_hi_outer('Hello')
closure('Robert')
closure('Paul')
In this example, the print_hi_inner(...) function is defined within the scope of the print_hi_outer(...) function. The inner function has access to the parameter message
of the outer function, even after the outer function has completed execution. This behavior is known as a “closure.” The print_hi_outer(...) function returns the print_hi_inner(...) function, creating a closure with the value of message
captured.
❯ python3 main.py
Hi, Robert
Hi, Paul
Hello, Robert
Hello, Paul
Inner functions are often used to encapsulate logic that’s only relevant to a specific context or to create custom behavior for different instances of a function. They help keep code organized, modular, and easier to understand by breaking down complex tasks into smaller, manageable pieces.
Global, local, and nonlocal
In Python, the visibility of a variable is drawn by the scope where it's been defined. In particular, we can differentiate three types of variables' scopes: local, nonlocal, and global.
Local variables are variables that are defined within a function and are only accessible within that function’s scope. They cannot be accessed from outside the function.
def my_function():
x = 10
print(x)
my_function()
print(x)
Here we have my_function(...) that defines the x
variable.
❯ python3 main.py
10
Traceback (most recent call last):
File "main.py", line 6, in <module>
print(x)
NameError: name 'x' is not defined
When executing the script we get the NameError
exception as the variable is not visible in this scope.
Global variables are variables defined outside of any function, at the top level of the script or module. They can be accessed from any part of the code, including within functions.
y = 20
def another_function():
print(y)
another_function()
The another_function(...) uses the y
variable that is defined outside of its body.
❯ python3 main.py
20
The script prints 20 and exits normally. Now we can slightly modify it, so before y
is printed, it is increased by one.
y = 20
def another_function():
y += 1
print(y)
another_function()
When we run the script, we encounter the following issue.
❯ python3 main.py
Traceback (most recent call last):
File "main.py", line 7, in <module>
another_function()
File "main.py", line 4, in another_function
y += 1
UnboundLocalError: local variable 'y' referenced before assignment
When executing the script we get the UnboundLocalError
exception as the variable is unknown before assignment. That's the part where the global
keyword comes into play.
y = 20
def another_function():
global y
y += 1
print(y)
another_function()
After declaring the y
variable as global
, we can run the script.
❯ python3 main.py
21
The script prints 21 and exits normally now.
Now consider the following code snippet.
def outer_function():
z = 30
def inner_function():
z += 5
print(z)
inner_function()
print(z)
outer_function()
We have the z
variable being increased by 5 in the inner function. The variable itself is defined in the outer function, yet not in the global scope.
❯ python3 main.py
Traceback (most recent call last):
File "main.py", line 12, in <module>
outer_function()
File "main.py", line 9, in outer_function
inner_function()
File "main.py", line 6, in inner_function
z += 5
UnboundLocalError: local variable 'z' referenced before assignment
When executing the script we get the UnboundLocalError
exception, similarly as in the previous situation. The remedy here is the nonlocal
keyword. It is used within a nested function to indicate that a variable belongs to the nearest enclosing scope that is not global. This is particularly useful when we have nested functions and want to modify a variable in an outer (but non-global) scope.
def outer_function():
z = 30
def inner_function():
nonlocal z
z += 5
print(z)
inner_function()
print(z)
outer_function()
Using nonlocal
fixes the code.
❯ python3 main.py
35
35
Type hints
Type hints in Python are a way to provide optional type information for variables, function parameters, and return values. They help improve code readability and maintainability, and can also be used by tools and IDEs to catch potential type-related errors before runtime. While Python is dynamically typed, type hints allow you to indicate the expected types of values, making the code more self-explanatory and aiding developers in understanding how functions and variables are supposed to be used.
Type hints are not enforced by the Python interpreter itself, meaning that the code will still run even if the type hints are incorrect. However, tools like "mypy" can be used to statically analyze our code and provide type checking based on the hints.
Here's an example of using type hints:
def add_numbers(a: int, b: int) -> int:
return a + b
result = add_numbers(5, 3)
print(result)
In this example:
a: int
andb: int
indicate that the parametersa
andb
are expected to be of typeint
.-> int
indicates that the return value of the function is expected to be anint
.
Python supports various types for type hints, including basic types like int
, str
, float
, and more complex types like List
, Dict
, Tuple
, and even user-defined classes.
Build-in functions
As with many other programming languages, Python has its own set of functions shipped out of the box. These functions are available for use without requiring any additional imports or installations. They cover a wide range of tasks and operations, making it easier to perform common actions and manipulations in code. Here are some examples of frequently used built-in functions:
Type Conversion Functions:
int()
: Converts a value to an integer.float()
: Converts a value to a floating-point number.str()
: Converts a value to a string.list()
: Converts an iterable to a list.tuple()
: Converts an iterable to a tuple.
Math Functions:
abs()
: Returns the absolute value of a number.round()
: Rounds a floating-point number to a specified number of decimal places.min()
: Returns the smallest item in an iterable.max()
: Returns the largest item in an iterable.sum()
: Returns the sum of items in an iterable.
String Manipulation Functions:
len()
: Returns the length of a string or iterable.str.lower()
: Converts a string to lowercase.str.upper()
: Converts a string to uppercase.str.capitalize()
: Capitalizes the first character of a string.str.split()
: Splits a string into a list based on a delimiter.
List and Tuple Functions:
len()
: Returns the length of a list or tuple.list.append()
: Adds an item to the end of a list.list.extend()
: Extends a list by appending elements from an iterable.list.pop()
: Removes and returns the last item from a list.tuple.index()
: Returns the index of the first occurrence of a value in a tuple.
Input and Output Functions:
input()
: Reads user input from the console.print()
: Displays output to the console.
Type Checking Functions:
type()
: Returns the type of an object.isinstance()
: Checks if an object is an instance of a specified class.
Iterating and Enumerating Functions:
range()
: Generates a sequence of numbers.enumerate()
: Returns an iterator that yields pairs of index and value.
File Handling Functions:
open()
: Opens a file for reading or writing.read()
: Reads the contents of a file.write()
: Writes data to a file.
These are just examples, but provide a foundation for performing various operations and are an integral part of writing efficient and expressive Python code.
Summary
In this post, we learned the basics of functions in Python.
Functions are modular blocks of code designed to perform specific tasks. They encapsulate a set of instructions under a meaningful name, making the code more readable, reusable, and manageable. Functions are defined with the def
keyword and can take multiple parameters and can also return multiple values.
Named parameters make code more self-explanatory, improve code readability, and reduce the potential for errors when calling functions with multiple parameters.
Default parameters are especially useful when you want to offer a sensible default behavior but still give callers the option to customize it. It can also simplify function calls by allowing you to omit arguments that use the default values.
Inner functions are frequently employed to contain logic that pertains solely to a particular context or to establish distinct behaviors for various instances of a function. This practice aids in maintaining organized, modular code that is simpler to comprehend, as it divides intricate tasks into smaller, more manageable components.
Type hints can be added to variables, function parameters, function return values, and even within classes. However, remember that type hints are optional and provide information for developers and tools, but they don’t change Python’s dynamic typing behavior.
Built-in functions are an integral part of Python and provide essential tools for a wide range of programming tasks. You can refer to the Python documentation for a comprehensive list and detailed information about each built-in function.