Functions in Python

By the Syncfusion DevTutorials team · Last updated

Writing and calling functions, every kind of argument, variable scope and the LEGB rule, packing and unpacking, annotations and docstrings.

Think about a kettle.

You fill it, switch it on, and it boils the water. You do not need to know how the element works, how the thermostat decides it is done, or what the wiring looks like. You just use it, and you use it again tomorrow.

Somebody built that once. Everybody uses it many times.

A function is the same idea in a program. You write a piece of work once, give it a name, and use it whenever you need it.

Write it once, use it three times

def grade_for(marks):
    if marks >= 80:
        return "A"
    if marks >= 60:
        return "B"
    return "C"

print(grade_for(90))
print(grade_for(78))
print(grade_for(45))

The grading rule is written once. It was used three times, and if the rule changes there is exactly one place to change it.

What You Will Learn

In this lesson, you will learn:

  • How to define a function, call it, and get an answer back
  • The different ways to pass values in, including *args and **kwargs
  • Where a variable can be seen, and the rule Python uses to find it
  • How packing and unpacking move values in and out of functions
  • The functions Python already gives you
  • How to label what a function expects and returns
  • How to describe a function for the next person who reads it

A function is a named piece of work you can run whenever you need it.

Introduction to Functions

What Is a Function?

A function is a block of code with a name. The code does not run when you write it. It runs when you call it by name.

Calling a function means running it. You call it by writing its name followed by brackets, like grade_for(78). The brackets tell Python to run the body now.

Why Functions Are Used

Without functions, repeated work gets copied.

The same rule, written three times

maths = 90
science = 78
english = 45

if maths >= 80:
    print("Maths: A")
elif maths >= 60:
    print("Maths: B")
else:
    print("Maths: C")

if science >= 80:
    print("Science: A")
elif science >= 60:
    print("Science: B")
else:
    print("Science: C")

if english >= 80:
    print("English: A")
elif english >= 60:
    print("English: B")
else:
    print("English: C")

Three copies of one rule. If the A boundary moves to 85, you have to change it in three places and remember all of them.

Benefits of Functions

  • One place to change. The rule lives in one block.
  • A name that explains. grade_for(marks) says what it does; fifteen lines of if do not.
  • Easier to test. You can try one function on its own.
  • Less to read. The detail is tucked away until you need it.

Function Syntax

def name(parameters):
    body
    return value
  • def tells Python a function is being defined
  • the name follows the same rules as a variable name
  • the brackets hold the parameters, which may be empty
  • the line ends in a colon, and the body is indented

Defining Functions

Defining a function does not run it

def print_header():
    print("Report card")
    print("-" * 20)

print("Nothing has happened yet")

Nothing from inside the function was printed. Defining a function only stores it under its name. The two lines inside it will run later, when somebody calls the function.

Calling Functions

Calling it runs the body.

Calling runs the body

def print_header():
    print("Report card")
    print("-" * 20)

print_header()
print("Asha - class 7B")
print_header()

The brackets are what make the call happen. A name with no brackets gives you the function itself, not its result:

With brackets and without

def print_header():
    print("Report card")

print(print_header)
print_header()

The first line printed something like <function print_header at 0x...>. That is the function itself. Forgetting the brackets is a common slip, and it produces no error at all.

Function Parameters

A parameter is a name in the definition. An argument is the value you pass in when you call it.

One parameter, three different arguments

def greet(student):
    print("Hello,", student)

greet("Asha")
greet("Ravi")
greet("Meera")

student is the parameter. "Asha" is an argument.

Think of it this way: the parameter is the empty box labelled in the definition; the argument is what you put in the box when you call.

Several parameters are separated by commas, and the values line up in order:

The values line up in order

def report(student, subject, marks):
    print(student, "scored", marks, "in", subject)

report("Asha", "Maths", 78)

Passing the wrong number of arguments is caught straight away:

Too few arguments

def report(student, subject, marks):
    print(student, subject, marks)

report("Asha", "Maths")

TypeError: report() missing 1 required positional argument: 'marks' names the parameter that had no value.

Return Values

return hands a value back to whoever called the function.

Handing a value back

def total_of(marks):
    return sum(marks)

result = total_of([78, 85, 60])

print("Total:", result)
print("Doubled:", result * 2)

The value arrived at the call, was stored in result, and the program went on using it. That is what returning means: the answer comes back to whoever asked.

print() shows something to a person. return gives a value to the rest of the program. They are not the same, and mixing them up is the most common beginner mistake with functions.

Printing is not returning

def shows(marks):
    print(sum(marks))

def gives(marks):
    return sum(marks)

a = shows([78, 85])
b = gives([78, 85])

print("From the printing one:", a)
print("From the returning one:", b)

a is None, because shows() printed a number and returned nothing. A function with no return always hands back None.

return also ends the function immediately:

Nothing after return runs

def grade_for(marks):
    if marks >= 80:
        return "A"
        print("This line can never run")
    return "Not an A"

print(grade_for(90))

The print under the first return never runs. The moment return is reached, the function is finished and the value goes back to the caller.

A function can return several values at once, separated by commas:

Returning more than one value

def summarise(marks):
    return min(marks), max(marks), sum(marks) / len(marks)

lowest, highest, average = summarise([78, 85, 60])

print("Lowest:", lowest)
print("Highest:", highest)
print("Average:", round(average, 1))

The three values are packed together on the way back and unpacked into three names at the call.

So far, every function has received a fixed set of values.

But functions are not limited to one style of input. Python provides several ways to pass values into a function, from simple positional arguments to more flexible approaches that can accept any number of values.

Function Arguments

Function Arguments Overview

There are several ways to pass values in. They exist so a function can stay easy to call as it grows.

KindLooks likeMeaning
Positionalreport("Asha", 78)matched by order
Keywordreport(student="Asha")matched by name
Defaultdef report(marks=0)used when nothing is passed
Positional-onlydef f(a, b, /)must be given by position
Keyword-onlydef f(*, a, b)must be given by name
*argsdef f(*marks)any number of positional values
**kwargsdef f(**details)any number of named values

The first three kinds — positional, keyword and default — cover most of the code you will write.

The next few argument styles are used less often. You may not need them in every program, but they are worth recognising, because you will see them in professional Python code. Each one is explained below as you meet it.

Positional Arguments

Positional arguments are matched by their order. The first value goes to the first parameter.

Order decides everything

def report(student, subject, marks):
    print(student, "scored", marks, "in", subject)

report("Asha", "Maths", 78)
report("Maths", "Asha", 78)

The second call is nonsense, and Python cannot tell. Both values are text, both fit, and nothing errors. Order is the only thing holding it together.

Keyword Arguments

A keyword argument names the parameter it is for, so order stops mattering.

Named values, any order

def report(student, subject, marks):
    print(student, "scored", marks, "in", subject)

report(subject="Maths", marks=78, student="Asha")

Keywords also make a call readable at a glance:

Which call explains itself?

def record(student, marks, attendance, promoted):
    print(student, marks, attendance, promoted)

record("Asha", 78, 92, True)
record(student="Asha", marks=78, attendance=92, promoted=True)

In the first call, the reader has to remember what each bare value means. In the second, the names say it.

Positional arguments must come before keyword ones:

A positional value after a keyword one

def report(student, subject, marks):
    print(student, subject, marks)

report(student="Asha", "Maths", 78)

Default Arguments

A default gives a parameter a value to use when the caller does not supply one.

A parameter that can be left out

def report(student, subject="Maths", marks=0):
    print(student, "scored", marks, "in", subject)

report("Asha")
report("Ravi", "Science")
report("Meera", "English", 92)

The first call needed only the student, because subject and marks had defaults waiting.

Parameters with defaults must come after those without:

A default before a required parameter

def report(subject="Maths", student):
    print(student, subject)

One rule is worth learning before it bites you: never use a list or a dictionary as a default value.

A default list is created once, not once per call

def add_mark(mark, marks=[]):
    marks.append(mark)
    return marks

print(add_mark(78))
print(add_mark(85))
print(add_mark(60))

Every call shares one list, because the default was created once when the function was defined. The usual fix is None:

The safe way to default a list

def add_mark(mark, marks=None):
    if marks is None:
        marks = []
    marks.append(mark)
    return marks

print(add_mark(78))
print(add_mark(85))

None means "nothing was passed". When the function sees it, it builds a fresh list itself, so no list is ever shared between calls.

A default value is created once, when the function is defined. For a number or a piece of text that is harmless. For a list or a dictionary it means every call shares the same one. Default to None and build it inside.

Positional-Only Arguments

Anything before a / in the parameter list must be passed by position.

A positional-only parameter passed by name

def divide(a, b, /):
    return a / b

print(divide(10, 2))
print(divide(a=10, b=2))

The first call worked. The second failed, because a and b may not be named.

This is used for parameters whose names carry no meaning for the caller, so the author stays free to rename them later.

Keyword-Only Arguments

Anything after a bare * must be passed by name.

Parameters that must be named

def record(student, *, promoted, attendance):
    print(student, "promoted:", promoted, "attendance:", attendance)

record("Asha", promoted=True, attendance=92)

A keyword-only parameter passed by position

def record(student, *, promoted, attendance):
    print(student, promoted, attendance)

record("Asha", True, 92)

This is how you stop a call turning into a row of bare values whose meaning nobody can read.

Arbitrary Positional Arguments

*args collects any number of positional values into one tuple.

Any number of values

def total(*marks):
    print("Got:", marks)
    return sum(marks)

print(total(78, 85))
print(total(78, 85, 60, 92))
print(total())

The star does the collecting. The name is a convention, not a rule — *marks works just as well as *args, and reads better.

Arbitrary Keyword Arguments

**kwargs collects any number of named values into a dictionary.

Any number of named values

def describe(**details):
    for key, value in details.items():
        print(key, "->", value)

describe(student="Asha", marks=78, section="7B")

Mixing Different Argument Types

They can be combined, but the order in the definition is fixed:

positional, *args, keyword-only, **kwargs

All the kinds in one definition

def report(student, *marks, promoted=False, **details):
    print("Student:", student)
    print("Marks:", marks)
    print("Promoted:", promoted)
    print("Details:", details)

report("Asha", 78, 85, 60, promoted=True, section="7B", teacher="Meera")

"Asha" filled the first parameter. The three numbers were collected by *marks. promoted was named. Everything else landed in details.

Most functions need none of this. Reach for it when a function genuinely takes an unknown number of values.

Passing values into a function is only half the story.

We also need to know what happens to variables once they are inside the function. This brings us to scope: the rules that decide where a variable can be seen and used.

Variable Scope

What Is Variable Scope?

When programs become larger, different functions often use variables with the same names.

Python needs rules to decide which variable a piece of code should use. Those rules are called scope: the part of a program where a name can be seen.

A name created inside a function belongs to that function. Outside it, the name does not exist.

Local Variables

A local variable is one created inside a function.

A local name does not escape

def total_of(marks):
    result = sum(marks)
    return result

print(total_of([78, 85]))
print(result)

NameError — result existed only while the function was running.

This is a feature, not a limit. It means two functions can both use a variable called result without ever interfering with each other.

Global Variables

A global variable is created outside every function. Functions can read it.

A function can read a global

PASS_MARK = 35

def has_passed(marks):
    return marks >= PASS_MARK

print(has_passed(78))
print(has_passed(20))

But assigning to it inside a function creates a new local name instead of changing the global one:

Assigning inside makes a local name

count = 0

def record():
    count = 99
    print("Inside the function:", count)

record()
print("Outside the function:", count)

The global count never changed. The function quietly made its own count, used it, and threw it away when the call ended.

The global Keyword

global says that a name inside the function refers to the one outside.

Changing a global on purpose

count = 0

def record():
    global count
    count += 1

record()
record()
record()

print("Records made:", count)

Use it sparingly. A function that changes a global can affect any other part of the program, which makes a bug hard to trace. Returning a value is almost always better:

The same job without global

def record(count):
    return count + 1

count = 0
count = record(count)
count = record(count)

print("Records made:", count)

Here the function takes the value in and hands the new one back, so every change is visible at the call where it happens.

The nonlocal Keyword

A function can be defined inside another function. nonlocal lets the inner one change a name belonging to the outer one.

Reaching the enclosing function's name

def counter():
    count = 0

    def increase():
        nonlocal count
        count += 1

    increase()
    increase()
    return count

print(counter())

Without nonlocal, count += 1 would try to make a local name and fail, because it reads the value before setting it.

global reaches all the way out to the module. nonlocal reaches out one level, to the enclosing function.

Scope Resolution (LEGB Rule)

When Python meets a name, it looks in four places, in this order:

LetterScopeWhere it is
LLocalinside the current function
EEnclosinginside the function that contains it
GGlobalat the top level of the file
BBuilt-innames Python always provides, like print

The first match wins, and the search stops there.

All four levels, innermost first

student = "global Asha"

def outer():
    student = "enclosing Ravi"

    def inner():
        student = "local Meera"
        print(student)

    inner()
    print(student)

outer()
print(student)

Three names, three values, no interference. Each print found the nearest one.

This is also why a variable named after a built-in causes trouble:

Shadowing a built-in name

list = [78, 85]
print(list)

numbers = list(range(3))

list was found in the global scope before Python ever reached the built-ins, so list(...) tried to call a list. Avoid naming variables list, sum, str, type or input.

The practical rule is simple: Python looks for the nearest matching name first. If it cannot find one, it keeps looking further out until it either finds a match or runs out of places to search.

Several of the argument styles you have just seen use symbols such as * and **. To understand exactly what those symbols are doing, we need to look at packing and unpacking.

Packing and Unpacking

What Is Packing?

Packing gathers several values into one.

Several values becoming one tuple

result = "maths", 78, "A"

print(result)
print(type(result))

Packing with *args

In a function definition, * packs the extra positional arguments into a tuple.

Packing the arguments as they arrive

def total(*marks):
    print("Packed into:", marks)
    return sum(marks)

print(total(78, 85, 60))

What Is Unpacking?

Unpacking is the opposite: taking one collection apart into separate values.

Taking a tuple apart

result = ("maths", 78, "A")

subject, marks, grade = result

print(subject)
print(marks)
print(grade)

Unpacking Iterables

A * on the left collects everything that is left over.

First, last, and the rest

marks = [78, 85, 60, 92, 45]

first, *middle, last = marks

print("First:", first)
print("Middle:", middle)
print("Last:", last)

middle is always a list, even when it ends up holding one item or none.

Unpacking Function Arguments

A * at the call spreads a list out into separate arguments.

Spreading a list into a call

def report(student, subject, marks):
    print(student, "scored", marks, "in", subject)

values = ["Asha", "Maths", 78]

report(*values)

Without the *, the whole list would arrive as a single first argument.

** does the same for a dictionary, matching the keys to parameter names:

Spreading a dictionary into a call

def report(student, subject, marks):
    print(student, "scored", marks, "in", subject)

details = {"student": "Asha", "subject": "Maths", "marks": 78}

report(**details)

So * and ** mean opposite things depending on where they are: in a definition they pack, and at a call they unpack.

Multiple Assignment Unpacking

The same mechanism handles the everyday swap:

Swapping two values with no temporary

first = 78
second = 85

first, second = second, first

print(first, second)

The right-hand side is packed into a pair first, then unpacked into the two names. No third variable is needed.

A quick way to remember it:

  • In a function definition, * and ** collect values together.
  • In a function call, * and ** spread values apart.

So far, we have been writing our own functions.

However, Python already includes many useful functions that are ready to use without writing them yourself.

Built-in Functions

What Are Built-in Functions?

A built-in function is one Python provides without any import. You have been using them since the first lesson.

Built-ins you have already met

marks = [78, 85, 60]

print(len(marks))
print(sum(marks))
print(max(marks))
print(min(marks))
print(sorted(marks))

Benefits of Built-in Functions

They are already written, already tested, and faster than anything you would write yourself. Reaching for one is almost always better than writing the loop by hand.

By hand, and with a built-in

marks = [78, 85, 60]

total = 0
for mark in marks:
    total += mark
print("By hand:", total)

print("Built-in:", sum(marks))

Categories of Built-in Functions

GroupFunctions
Numbersabs(), round(), pow(), sum(), min(), max()
Typesint(), float(), str(), bool(), list(), dict(), type()
Collectionslen(), sorted(), reversed(), enumerate(), zip(), range()
Checkingisinstance(), all(), any()
Input and outputprint(), input()
Finding outdir(), help()

Three worth knowing early

marks = [78, 85, 60]
subjects = ["Maths", "Science", "English"]

print(all(mark >= 35 for mark in marks))
print(any(mark >= 80 for mark in marks))

for subject, mark in zip(subjects, marks):
    print(subject, mark)

all() asks whether every item passes. any() asks whether at least one does. zip() walks two collections side by side.

Functions already work without any extra information.

However, as programs grow larger, it becomes useful to describe what values a function expects and what it returns. Function annotations help with this.

Function Annotations

What Are Function Annotations?

An annotation labels what a parameter is expected to hold and what the function gives back.

Python does not check them. They are notes for people and for tools.

Parameter Annotations

Labelling what goes in

def greet(student: str):
    print("Hello,", student)

greet("Asha")
greet(78)

The second call passed a number where the annotation says text, and Python ran it anyway. The label is a note, not a rule.

Return Type Annotations

An arrow labels what comes back.

Labelling what comes out

def grade_for(marks: int) -> str:
    if marks >= 80:
        return "A"
    return "B"

print(grade_for(90))

Accessing Function Annotations

Python stores them, so tools can read them.

The annotations are kept

def report(student: str, marks: int) -> str:
    return f"{student}: {marks}"

print(report.__annotations__)

Benefits of Function Annotations

  • The signature says what is expected, with no comment needed.
  • Editors use them for better suggestions and warnings.
  • Checking tools such as mypy can find mismatches before the code runs.
  • They survive. A comment drifts out of date; an annotation sits in the signature where it is read.

An annotated function reads like a contract

def average_of(marks: list[int]) -> float:
    return sum(marks) / len(marks)

print(average_of([78, 85, 60]))

Anyone can see it takes a list of whole numbers and gives back a decimal, without reading the body.

Annotations help describe the values a function expects.

Docstrings take that idea further by allowing you to describe the purpose of the function itself.

Docstrings

What Is a Docstring?

A docstring is a piece of text, written as the first thing inside a function, that says what the function does.

It is not a comment. Python keeps it, and tools show it.

A function that describes itself

def grade_for(marks):
    """Return the letter grade for a mark out of 100."""
    if marks >= 80:
        return "A"
    if marks >= 60:
        return "B"
    return "C"

print(grade_for(78))
print(grade_for.__doc__)

Writing Docstrings

Use triple quotes, even for one line, so it can grow later.

For anything beyond the obvious, say what goes in and what comes back:

A longer docstring

def average_of(marks):
    """Return the average of a list of marks.

    marks: a list of numbers, which must not be empty.
    Returns the average as a decimal number.
    """
    return sum(marks) / len(marks)

print(average_of([78, 85, 60]))
print(average_of.__doc__)

Say why and what, not how. The body already says how.

Which docstring earns its place?

def grade_for(marks):
    """Check the marks with if statements and return a letter."""
    return "A" if marks >= 80 else "B"

def promoted(marks, attendance):
    """Decide whether a student moves up a year.

    A student is promoted only when they pass the exam and
    attend at least 75 percent of classes, which is the rule
    agreed with the class teachers.
    """
    return marks >= 35 and attendance >= 75

print(grade_for(90), promoted(78, 92))

The first repeats what anyone can see. The second records a rule that comes from outside the code, which is exactly what a reader cannot work out for themselves.

Write the docstring before the body. If you cannot say in one sentence what the function returns, the function is probably doing two things.

Common mistakes

Each row is one mistake. The middle column is what you will actually see on screen.

MistakeWhat you seeWhy it happensFix
Printing instead of returningThe caller gets Noneprint() shows a value; return hands it backUse return, and print the result at the call
Calling without brackets<function grade_for at 0x...>The name alone is the function, not a callgrade_for(78)
Wrong number of argumentsTypeError: missing 1 required positional argumentEvery parameter without a default needs a valuePass all of them, or give defaults
A list as a default valueValues pile up between callsThe default is created once, at definitionDefault to None and build it inside
A default before a required parameterSyntaxError: non-default argument follows default argumentRequired parameters must come firstMove the defaults to the end
A positional value after a keyword oneSyntaxError: positional argument follows keyword argumentPositional values must come firstReorder the call
Assigning to a global inside a functionThe global never changesAssignment makes a new local nameReturn a value, or use global deliberately
Using a local name outside its functionNameError: name 'result' is not definedA local name ends with the callReturn it
Naming a variable list or sumTypeError: 'list' object is not callableThe global name is found before the built-inPick another name
Expecting annotations to be enforcedA wrong type passes straight throughPython does not check annotationsUse a checker such as mypy
Code after a returnA line that never runsreturn ends the function at onceMove it above the return

What to Remember

  • A function is a named piece of work you can run whenever you need it.
  • def defines it; brackets call it. A name without brackets is the function itself, not its result.
  • A parameter is the name in the definition; an argument is the value passed in.
  • print() shows a value; return hands it back. A function with no return gives None.
  • Positional arguments are matched by order, keyword arguments by name.
  • A default lets a parameter be left out, and defaults come after required parameters.
  • A list or dictionary default is created once and shared by every call. Default to None.
  • *args collects extra positional values; **kwargs collects extra named ones.
  • * and ** pack in a definition and unpack at a call.
  • A name created in a function is local and disappears when the call ends.
  • Reading a global needs nothing; changing one needs global, and returning a value is usually better.
  • Python finds names Local, Enclosing, Global, Built-in — in that order, first match wins.
  • Annotations label what goes in and comes out, and Python does not enforce them.
  • A docstring says what a function does and why, and Python keeps it.

Next: Code Organization