Python Basics series — Part 6. This post continues Part 1: What Every Beginner Should Know, Part 2: Variables and Data Types, Part 3: Lists and Dictionaries, Part 4: If Statement, and Part 5: User Input and While Loops. Each post in the series takes one topic and covers it in depth. This one covers functions.
Look back at the programs from earlier in this series and you’ll notice something: the same patterns keep showing up. Ask a question, clean the answer, check it, ask again if it’s wrong. Add up a list of prices. Format a number as dollars. Every time you need one of those again, you could copy and paste the code — but then a bug fixed in one copy is still broken in all the others, and your programs grow longer and harder to read.
Functions solve this. A function is a named block of code that does one job. You write it once, give it a clear name, and then call it whenever you need that job done. You’ve been using functions since the very first post — print(), input(), len(), sorted(), and round() are all functions that come with Python. In this post, you’ll learn to write your own.
As always, open the Python shell (python3 in your terminal), or save the examples in a .py file, and try each one yourself.
Defining and Calling a Function
You create a function with the def keyword:
def greet():
"""Print a short welcome message."""
print("Welcome to Bright Bakery!")
print("Fresh bread every morning.")
Here’s what each part does:
deftells Python you’re defining a function.greetis the function’s name. Function names follow the same rules as variable names, written insnake_case. Good function names usually start with a verb:calculate_total,send_email,get_price.- The parentheses
()hold the function’s inputs. This one doesn’t need any, so they’re empty — but they’re still required. - The colon and the indented block below it are the function’s body: the code that runs when the function is called.
- The text in triple quotes is a docstring: a short description of what the function does. It’s optional, but it’s a great habit. Editors show it when you hover over the function, and
help(greet)prints it.
Defining a function doesn’t run it. To run it, call the function by writing its name followed by parentheses:
greet()
greet()
Welcome to Bright Bakery!
Fresh bread every morning.
Welcome to Bright Bakery!
Fresh bread every morning.
Python reads your file from top to bottom, so a function must be defined before you call it.
Parameters and Arguments
Most functions need some information to do their job. You list the inputs inside the parentheses as parameters:
def greet(name):
"""Greet a person by name."""
print(f"Welcome, {name}!")
greet("Maria")
greet("Carlos")
Welcome, Maria!
Welcome, Carlos!
The two words are often mixed up, so here’s the difference:
- A parameter is the variable name in the function’s definition:
name. - An argument is the actual value you pass in when you call it:
"Maria".
Several Parameters
Separate multiple parameters with commas. When you call the function, the arguments are matched to the parameters in order. These are called positional arguments:
def describe_project(client, pages):
print(f"{client} needs a {pages}-page website.")
describe_project("Bright Bakery", 5)
# Bright Bakery needs a 5-page website.
Order matters. Swap the arguments and you get nonsense:
describe_project(5, "Bright Bakery")
# 5 needs a Bright Bakery-page website.
If you pass the wrong number of arguments, Python stops with a TypeError that tells you exactly what’s missing:
describe_project("Bright Bakery")
# TypeError: describe_project() missing 1 required positional argument: 'pages'
Keyword Arguments
You can also pass arguments by name. These are called keyword arguments, and with them, the order doesn’t matter:
describe_project(pages=5, client="Bright Bakery")
# Bright Bakery needs a 5-page website.
Keyword arguments make calls easier to read, especially when a function takes several values and it’s not obvious which is which. You can mix both styles, but positional arguments have to come first.
Default Values
A parameter can have a default value, which is used when the caller doesn’t provide one. This makes the argument optional:
def describe_project(client, pages, project_type="website"):
print(f"{client} needs a {pages}-page {project_type}.")
describe_project("Bright Bakery", 5)
describe_project("Valley Dental", 1, "landing page")
describe_project("Calexico Coffee", 12, project_type="online store")
Bright Bakery needs a 5-page website.
Valley Dental needs a 1-page landing page.
Calexico Coffee needs a 12-page online store.
Parameters with defaults must come after the ones without defaults. def describe_project(project_type="website", client, pages): is a SyntaxError.
Return Values
So far, these functions have printed their results. That’s fine for showing a message, but most of the time you want a function to give back a result so the rest of your program can use it — store it in a variable, do more math with it, or pass it to another function. That’s what return does:
def add_tax(price, tax_rate=0.0775):
"""Return the price with sales tax added, rounded to cents."""
return round(price * (1 + tax_rate), 2)
total = add_tax(100)
print(total) # 107.75
print(add_tax(19.99)) # 21.54
print(add_tax(100) * 3) # 323.25
When Python reaches return, the function ends immediately and the value after return becomes the result of the call. add_tax(100) is replaced by 107.75, just like len([1, 2, 3]) is replaced by 3.
Return vs. Print
This is one of the most common points of confusion for beginners, so it’s worth seeing side by side:
def show_total(price):
print(price * 2) # displays the value, then throws it away
def get_total(price):
return price * 2 # hands the value back to the caller
a = show_total(10) # prints 20
b = get_total(10) # prints nothing
print(a) # None
print(b) # 20
show_total() displays 20 on the screen, but the value goes nowhere. A function that doesn’t reach a return statement automatically returns None, so a ends up empty. get_total() doesn’t print anything, but b holds the actual result.
A good rule of thumb: let functions return values, and do the printing in the code that calls them. Functions that return values are easier to reuse and test.
Returning Early
Because return ends the function, you can use it to exit as soon as you know the answer. This often makes if statements simpler:
def shipping_cost(order_total, is_member=False):
"""Return the shipping cost for an order."""
if is_member:
return 0
if order_total >= 75:
return 0
return 7.99
print(shipping_cost(120)) # 0
print(shipping_cost(40, is_member=True)) # 0
print(shipping_cost(40)) # 7.99
Returning More Than One Value
A function can return several values separated by commas. Python packs them into a tuple, which you can unpack into separate variables, just like you saw in Part 2:
def price_range(prices):
"""Return the lowest and highest price in a list."""
return min(prices), max(prices)
low, high = price_range([49, 19, 99, 5])
print(f"Prices from ${low} to ${high}") # Prices from $5 to $99
Returning Lists and Dictionaries
A function can return any type of value, including the lists and dictionaries from Part 3. This is a common way to bundle related information together:
def build_client(name, business, email=""):
"""Return a dictionary describing a client."""
client = {"name": name.strip().title(), "business": business.strip()}
if email:
client["email"] = email.strip().lower()
return client
print(build_client(" maria lopez ", "Bright Bakery"))
# {'name': 'Maria Lopez', 'business': 'Bright Bakery'}
print(build_client("carlos", "Calexico Coffee", "[email protected]"))
# {'name': 'Carlos', 'business': 'Calexico Coffee', 'email': '[email protected]'}
The empty-string default for email makes it optional: the key is only added when an email is provided.
Functions and Lists: Watch Out for Changes
In Part 2, you learned that lists and dictionaries are mutable, and that two names can point to the same list. The same is true when you pass a list to a function: the function gets the original list, not a copy. If the function changes it, the change is visible everywhere:
def mark_all_done(tasks):
for i in range(len(tasks)):
tasks[i] = tasks[i] + " ✓"
my_tasks = ["Design", "Build", "Launch"]
mark_all_done(my_tasks)
print(my_tasks) # ['Design ✓', 'Build ✓', 'Launch ✓']
Sometimes that’s exactly what you want. When it isn’t, have the function build and return a new list instead, and leave the original alone:
def with_checkmarks(tasks):
return [task + " ✓" for task in tasks]
my_tasks = ["Design", "Build", "Launch"]
done = with_checkmarks(my_tasks)
print(my_tasks) # ['Design', 'Build', 'Launch']
print(done) # ['Design ✓', 'Build ✓', 'Launch ✓']
Variable Scope: Local and Global
Variables created inside a function are local: they only exist while the function runs, and the rest of your program can’t see them:
def calculate_quote(pages):
hours = pages * 4 # local variable
return hours * 85
print(calculate_quote(5)) # 1700
print(hours) # NameError: name 'hours' is not defined
This is a good thing. It means you can use simple names like total or count inside different functions without them interfering with each other.
Variables created outside any function are global, and functions can read them. That’s the usual way to share constants:
HOURLY_RATE = 85
def calculate_quote(pages):
return pages * 4 * HOURLY_RATE
print(calculate_quote(5)) # 1700
Reading a global constant is fine. Changing a global variable from inside a function, though, is a habit to avoid — it makes programs hard to follow, because any function might be quietly changing shared data. Pass values in as arguments and get results out with return instead.
Flexible Arguments: *args and **kwargs
Sometimes you don’t know in advance how many arguments a function will receive. Python has two special parameter forms for this.
*args collects any number of extra positional arguments into a tuple:
def total_cost(*prices):
"""Add up any number of prices."""
return sum(prices)
print(total_cost(19.99, 5.00)) # 24.99
print(total_cost(100, 250, 49, 12)) # 411
**kwargs collects any number of extra keyword arguments into a dictionary:
def build_profile(name, **details):
"""Return a profile dictionary with any extra details provided."""
profile = {"name": name}
profile.update(details)
return profile
print(build_profile("Maria", city="El Centro", plan="Business"))
# {'name': 'Maria', 'city': 'El Centro', 'plan': 'Business'}
The names args and kwargs are just conventions — what matters is the * and **. You won’t need these often as a beginner, but you’ll see them in other people’s code, so it helps to recognize them.
Lambda Functions
In Part 3, you used lambda to tell sorted() and max() what to compare. Now that you know functions, here’s what it actually is: a lambda is a small, unnamed function written in a single expression. These two are equivalent:
def get_budget(project):
return project["budget"]
get_budget = lambda project: project["budget"]
Lambdas are handy when you need a tiny function just once, usually as an argument to another function:
projects = [
{"client": "Bright Bakery", "budget": 2500},
{"client": "Calexico Coffee", "budget": 3200},
{"client": "Valley Dental", "budget": 300},
]
by_budget = sorted(projects, key=lambda p: p["budget"], reverse=True)
print([p["client"] for p in by_budget])
# ['Calexico Coffee', 'Bright Bakery', 'Valley Dental']
For anything more than a single short expression, write a regular function with def — it’s easier to read, and it can have a docstring.
Organizing Functions into Modules
As your collection of functions grows, you can move them into their own file, called a module, and import them wherever you need them. Save these functions in a file named quote_tools.py:
# quote_tools.py
HOURLY_RATE = 85
def calculate_quote(pages, rush=False):
"""Return the price for a website with the given number of pages."""
total = pages * 4 * HOURLY_RATE
if rush:
total = total * 1.25
return total
def format_money(amount):
"""Return an amount formatted as dollars, like $1,700.00."""
return f"${amount:,.2f}"
Then, in another file in the same folder, import and use them:
# main.py
import quote_tools
price = quote_tools.calculate_quote(5, rush=True)
print(quote_tools.format_money(price)) # $2,125.00
There are a few ways to import:
import quote_tools # use as quote_tools.calculate_quote()
from quote_tools import calculate_quote # use as calculate_quote()
from quote_tools import calculate_quote, format_money
import quote_tools as qt # use as qt.calculate_quote()
This is exactly how you’ve been using Python’s built-in modules, like json, copy, and collections, and how you’ll use libraries such as pandas later on.
Putting It All Together: A Reusable Quote Calculator
In Part 5, the project intake tool had several while True loops that all did almost the same thing: ask a question, check the answer, ask again if it’s wrong. Functions let you write that logic once and reuse it:
HOURLY_RATE = 85
HOURS_PER_PAGE = 4
RUSH_FEE = 0.25
def ask_text(prompt):
"""Ask until the user types something that isn't empty."""
while True:
answer = input(prompt).strip()
if answer:
return answer
print("This can't be empty.")
def ask_number(prompt, minimum=1):
"""Ask until the user types a whole number of at least `minimum`."""
while True:
answer = input(prompt).strip()
if answer.isdigit() and int(answer) >= minimum:
return int(answer)
print(f"Please enter a whole number of at least {minimum}.")
def ask_yes_no(prompt):
"""Ask a yes/no question and return True for yes, False for no."""
while True:
answer = input(f"{prompt} (yes/no) ").strip().lower()
if answer in ("yes", "y"):
return True
if answer in ("no", "n"):
return False
print("Please answer yes or no.")
def calculate_quote(pages, rush=False):
"""Return a dictionary with the hours, fee, and total for a project."""
hours = pages * HOURS_PER_PAGE
subtotal = hours * HOURLY_RATE
rush_fee = subtotal * RUSH_FEE if rush else 0
return {"hours": hours, "subtotal": subtotal, "rush_fee": rush_fee,
"total": subtotal + rush_fee}
def format_money(amount):
"""Return an amount formatted as dollars."""
return f"${amount:,.2f}"
def print_quote(business, quote):
"""Print a formatted quote."""
print(f"\nQuote for {business}")
print(f" Hours: {quote['hours']}")
print(f" Subtotal: {format_money(quote['subtotal'])}")
if quote["rush_fee"]:
print(f" Rush fee: {format_money(quote['rush_fee'])}")
print(f" Total: {format_money(quote['total'])}")
business = ask_text("Business name: ")
pages = ask_number("How many pages? ")
rush = ask_yes_no("Do you need it within 2 weeks?")
quote = calculate_quote(pages, rush)
print_quote(business, quote)
A sample session:
Business name: Bright Bakery
How many pages? 0
Please enter a whole number of at least 1.
How many pages? 5
Do you need it within 2 weeks? (yes/no) y
Quote for Bright Bakery
Hours: 20
Subtotal: $1,700.00
Rush fee: $425.00
Total: $2,125.00
Look at the last five lines: they read almost like plain English. Each function does one job and has a name that says what that job is. To ask another yes/no question, you call ask_yes_no() again instead of writing another loop. To change how money is displayed, you edit format_money() in one place. And because calculate_quote() returns a dictionary instead of printing, you could reuse it in a web page, an email, or a spreadsheet export without changing a line.
Common Beginner Mistakes
Forgetting the parentheses when calling. greet refers to the function itself; greet() runs it. print(greet) displays something like <function greet at 0x...> instead of calling it.
Printing instead of returning. If a function prints its result, you can’t use that result anywhere else, and the call gives you None. Return the value and print it outside.
Forgetting the return statement. A function that calculates a value but never returns it hands back None. If you see None where you expected a result, check for a missing return.
Passing arguments in the wrong order. Positional arguments match parameters by position. Use keyword arguments when the order isn’t obvious.
Calling a function before it’s defined. Python runs files from top to bottom, so the def must come before the first call, or you’ll get a NameError.
Using a list or dictionary as a default value. Default values are created once, when the function is defined — not each time it’s called. A mutable default like [] is shared between calls:
def add_task(task, tasks=[]):
tasks.append(task)
return tasks
print(add_task("Design")) # ['Design']
print(add_task("Build")) # ['Design', 'Build'] ← the first task is still there!
Use None as the default and create a new list inside the function:
def add_task(task, tasks=None):
if tasks is None:
tasks = []
tasks.append(task)
return tasks
print(add_task("Design")) # ['Design']
print(add_task("Build")) # ['Build']
Making one function do everything. A function called process_everything() that asks questions, calculates, formats, and prints is hard to reuse and hard to fix. If you struggle to name a function, it’s probably doing too much — split it into smaller functions that each do one job.
Practice Exercises
- Write a function
celsius_to_fahrenheit(c)that returns the converted temperature (c * 9 / 5 + 32). Call it with a few values and print the results. - Write a function
full_name(first, last, middle="")that returns a properly capitalized full name, including the middle name only when one is given. - Write a function
is_valid_email(email)that returnsTrueif the email contains an@and a.after it, andFalseotherwise. Use it in awhileloop that asks for an email until it’s valid. - Write a function
average(numbers)that returns the average of a list, or0if the list is empty. - Write a function
count_words(text)that returns a dictionary of how many times each word appears. (Hint: combine.lower(),.split(), and the counting pattern from Part 3.) - Take a program you wrote for an earlier exercise in this series and reorganize it into functions, then move them into a module and import it.
For more practice, the 100 Days of Python Challenge projects lean on functions more and more as they get bigger.
Conclusion
Functions are how you turn a long script into an organized program. A function packages one job behind a clear name. Parameters let it work with different data, default values make some of those inputs optional, and return hands the result back so the rest of your program can use it. Local scope keeps each function’s variables to itself, and modules let you share functions between files.
With functions, you now have all the core building blocks from Part 1: variables and data types, lists and dictionaries, decisions with if, repetition with loops, user input, and now reusable functions. Together, they’re enough to build real programs that solve real problems. Pick a small, annoying task from your own work and write a program to handle it.
Next in the Python Basics series: Classes, how to bundle data and the functions that work on it into your own types.
Python Basics series: Part 1: What Every Beginner Should Know · Part 2: Variables and Data Types · Part 3: Lists and Dictionaries · Part 4: If Statement · Part 5: User Input and While Loops · Part 6: Functions · Part 7: Classes · Part 8: Files and Exceptions · Part 9: Testing Your Code
