Welcome back to our Python course. Today, we are talking about user-defined functions.
Why Do We Need Functions? The DRY Principle
Let’s first understand why we need user-defined functions. The problem is that we often have repetitive code; you have a few lines of code that appear again and again.
Let’s say you want to open a dialogue box on the screen, ask the user to enter something, and then press OK or cancel. This piece of code you will use perhaps dozens of times, perhaps hundreds of times if it’s a big program, where again and again, you will be opening different dialogue boxes. If you think about it, a dialogue box is a lot of stuff. You have to draw the buttons, draw the frame, open it, and check which button was pressed. You don’t want to reprogram that from scratch every single time.
This applies not only to user interface elements but to all kinds of code. You could have some calculation, let’s say you have to calculate the mean of a series of numbers, or you have to calculate some kind of arithmetic function that is specific to your program and does not exist as a library. You have to do this calculation again and again.
This is why you would put the code for this task into a separate block. It performs a function, which is why it’s called a function. To make it easy to address, you give it a name. It becomes a piece of code that has a name, and you can call this piece of code from everywhere else in your program. You write it once, you call it, it’s executed, and it returns to the place where you called it from. Later, you can call it from another place again; it will go execute this piece of code and return to the place where you called it from the second time. It is flexible where it returns; it doesn’t go back to the same place every time, which would not be so useful.
This is a very nice way of shortening your code and making it easier to understand. This also has an effect on errors in your program; it makes it easier to avoid them. If you make a mistake in the function, you have to fix it only once. If you make the same mistake in 10 different places where you copied the same code, you have to correct it in 10 different places.
There is a principle from programming which is sometimes abbreviated as DRY: Don’t Repeat Yourself. If you see that you’re going to write the same code twice in a program, you should already think, “I should have a function do this instead.”
The Anatomy of a Python Function
A function is a named, reusable piece of code that performs a single, specific task. You can think of it as a recipe: you define it once, give it a name, and then “cook” it (execute its instructions) whenever you like, just by calling its name. Functions make code organized, reliable, and much easier to maintain.
If you think of it, what you are already using in your program, like print and input, are functions just like the ones you will write. When you want to print something, lots of things happen in the background. The print function takes your string, finds a place to print it, and communicates with your screen or the Colab environment. This is something that somebody has programmed, and it is encapsulated in the print function.
A very simple function that just prints a welcome message would look like this:
def show_welcome_message():
"""A simple function that prints a welcome message."""
print("Welcome to the Digital Humanities Project!")
print("---------------------------------------")
# The code above only DEFINES the function.
# It is not executed until we CALL it.
# Now, we call the function.
show_welcome_message()
show_welcome_message()
When Python sees the def code, it will remember what the function does, but it will not execute it until we call it by name.
Let’s break down the anatomy:
defkeyword: This tells Python that a function definition is starting.Function Name: A descriptive name for the function (e.g.,
show_welcome_message). It follows the same naming rules as variables.Parentheses
(): These are required to mark it as a function. They will hold the function’s parameters, if any.Colon
:: This marks the end of the function’s header and the beginning of its code block.Docstring (Optional but Recommended): The string enclosed in triple quotes ("""
...""") is a documentation string, or “docstring.” It’s a convention to describe what the function does. Tools can automatically extract these to create documentation for your program.Indented Block: The body of the function. This is the code that runs every time the function is called. The indentation is how Python knows where the function’s code begins and ends.
Passing Information to Functions: Parameters
Many functions need some data to work on. For example, print needs to know what to print. Our functions can do the same by taking arguments, or parameters.
Here, we have a function greet_author that takes an author_name as a parameter and then prints a customized message.
def greet_author(author_name):
"""Greets a specific author by name."""
print(f"Hello, {author_name}. Welcome to the workshop!")
# Now we call the function and pass it different arguments.
greet_author("Virginia Woolf")
greet_author("James Baldwin")
The variable author_name is created the moment we call the function and exists only as long as the function is running. It does not exist outside of the function. If you were to add print(author_name) after the function calls, you would get an error because the variable is local to the function.
(A note on the backslash \): In the video, I sometimes use a backslash to break a long line of code across multiple lines to make it fit on the screen. It signals to Python that the line logically continues. For example:
print("This is a very long line that I have chosen to break " \
"into two for readability.")
Getting Information Back: The return Statement
Until now, our functions have just directly printed something. But what if you want a function to give you a value back? For example, a function that adds two numbers should give you the sum. Since all variables inside a function are destroyed when it finishes, we need a way to send a value back.
This is what the return statement is for. It tells the function what value to send back to the caller. You know this from the input() function, which returns the text the user entered.
def add_numbers(num1, num2):
"""Calculates the sum of two numbers and returns the result."""
result = num1 + num2
return result
# --- Using the returned value ---
# 1. Store the result in a variable for later use.
sum_of_years = add_numbers(1882, 35)
print(f"The sum is: {sum_of_years}")
# 2. Use the result directly, for example, in another function call.
print(f"The calculated value is: {add_numbers(1941, 4)}")
When you call the function, you can imagine the function call itself is replaced by the value it returns. Whether you store the value in a variable or use it directly depends on your use case. If you need the value multiple times, store it. If you only need it once, using it directly is fine.
Returning Multiple Values
A very convenient feature of Python is that functions can return multiple values at once. You just list them after the return statement, separated by commas.
Here is an example of a function that takes a bibliographic entry as a single string, splits it into parts, and returns the full name and the year as two separate values.
def parse_author_entry(entry_string):
# This function expects a string like: “Last, First, Year”
# It’s always good to document the expected format.
# The .split() method breaks the string into a list of parts.
# We can unpack this list directly into three variables.
last, first, year = entry_string.split(", ")
# We can use an f-string to create the full name.
full_name = f"{first} {last}"
# Return two values: the name and the year (converted to an integer).
return full_name, int(year)
# Now, let’s call the function and assign its two return values
# to two new variables.
name, birth_year = parse_author_entry("Woolf, Virginia, 1882")
print(f"Author Name: {name}")
print(f"Birth Year: {birth_year}")
The first value returned (full_name) goes into the first variable (name), and the second value returned (int(year)) goes into the second variable (birth_year). The variable names inside the function (full_name) and outside (name) do not need to match.
A Practical Example: Formatting Citations
Very often in the humanities, we have to reformat citations for different journals. A function is perfect for this kind of task. Here is a simple demonstration.
def format_citation(author, year):
"""Formats an author and year into a standard citation style."""
# We use an f-string to build the desired format.
citation_string = f"{author} ({year})."
return citation_string
# Get the data from our previous function
name, birth_year = parse_author_entry("Baldwin, James, 1924")
# Now use our new function to format it
citation_1 = format_citation(name, birth_year)
print(citation_1)
# We can do it again with different data
citation_2 = format_citation("Virginia Woolf", 1882)
print(citation_2)A Note on Style: Pure Functions vs. Side Effects
As a matter of good style, we generally avoid printing things directly inside a function (unless the function’s only job is to print).
The problem is that when you print from within a function, it becomes less universally usable. You are assuming that the moment the function is called is a good time to print something to the screen, which may not always be true. Sometimes you just want to calculate a value and use it later.
It is good style to have your function be a “pure function.” This means it gets some values, it returns a value, and it does not have any other “side effects” on your program—it doesn’t print, it doesn’t ask for input, and it doesn’t change any global state. This allows the function to be called from anywhere without causing unexpected interactions with the user. You leave the user interaction (like printing) to the main part of your program.
In this way, your functions become more modular and universally usable.
I hope you enjoyed this! — Thank you, and see you next time!


