Cover image for Python Functions Explained: Arguments, Returns, Scope, and Lambdas

At a glance

Reading time

~200 words/min

Published

6 hours ago

Jun 13, 2026

Views

3

All-time total

Python Functions Explained: Arguments, Returns, Scope, and Lambdas

Everything you have written so far has been a script: a recipe that runs top to bottom once. This lesson upgrades you to building tools. A function is a named block of code that accepts inputs, does one job, and hands back a result, and it changes how you program more than any other single feature. Functions let you stop copy-pasting, test logic in isolation, read a program as a table of contents instead of a wall of lines, and, from Part 6 onward, build the classes and modules that real software is made of.

There is also a quieter benefit worth naming. Functions force you to decide what a piece of code actually does, because you have to name it and define what goes in and what comes out. Programmers discover half their design mistakes at the moment of naming. If you cannot name a function honestly without using the word and, it is probably two functions, and that one-sentence rule will improve your code for the next decade.

What you will learn in Part 4

  • Defining functions with def, calling them, and returning values
  • Parameters, arguments, default values, and keyword arguments
  • Early returns, multiple return values, and what None really means
  • Local versus global scope, and why globals cause pain
  • Flexible signatures with *args and **kwargs
  • Docstrings, and small anonymous functions with lambda

Note

Before you start

You need conditions from Part 2 and loops from Part 3, because the example functions wrap exactly that kind of logic. If you can write FizzBuzz from memory, you are ready.

1. Defining and calling a function

A function definition starts with def, then the name, then a parenthesized list of parameters, then a colon and an indented body, the same block rule you mastered in Part 2. Running the def statement does not run the body; it creates the function and binds the name. The body runs only when you call the function, with parentheses, and each call is a fresh execution with fresh variables. The return statement ends the call immediately and sends a value back to whoever called.

def grade_for(score):
    """Return the letter grade for a 0-100 score."""
    if score >= 75:
        return "A"
    elif score >= 65:
        return "B"
    elif score >= 50:
        return "C"
    elif score >= 35:
        return "S"
    return "F"

print(grade_for(73))    # B
print(grade_for(91))    # A
result = grade_for(40)  # the returned value can be stored, printed, compared
print(result)           # S

This is the grade ladder from Part 2, but notice how the function version improves it. The logic now has a name, it can be reused for a thousand students without copying a line, and the early returns make the elif ladder flatter: once a return runs, the call is over, so each branch can simply state its answer. That string between triple quotes on the first line is a docstring, the standard way to document what a function does; editors show it when you hover, and help(grade_for) prints it. One honest sentence per function is a professional habit that costs five seconds.

2. Parameters, defaults, and keyword arguments

The names in the def line are parameters; the values you pass when calling are arguments. Python matches them by position by default, but you can also pass any argument by name, which makes calls self-documenting, and you can give parameters default values, which makes them optional. Defaults must come after non-defaults in the signature, and the combination of the two features covers most real-world flexibility you will ever need.

def make_greeting(name, punctuation="!", lang="en"):
    if lang == "si":
        return f"Ayubowan, {name}{punctuation}"
    return f"Hello, {name}{punctuation}"

print(make_greeting("Amina"))                       # Hello, Amina!
print(make_greeting("Amina", "?"))                  # Hello, Amina?
print(make_greeting("Amina", lang="si"))            # Ayubowan, Amina!
print(make_greeting(punctuation=".", name="Zane"))  # keyword order is free

One warning deserves a box of its own because it bites everyone exactly once: never use a mutable value like a list as a default. Defaults are evaluated one time, when the function is defined, so a default list is shared by every call that relies on it, and items mysteriously accumulate. The idiom is to default to None and create the list inside the function. You will understand the mechanics fully after Part 5; for now, adopt the rule.

# Wrong: the default list is created ONCE and shared
def add_item_bad(item, basket=[]):
    basket.append(item)
    return basket

# Right: make a fresh list per call
def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

print(add_item_bad("apple"))   # ['apple']
print(add_item_bad("mango"))   # ['apple', 'mango']  <- surprise!
print(add_item("apple"))       # ['apple']
print(add_item("mango"))       # ['mango']           <- correct

Checkpoint

A function has the signature def f(a, b=2, c=3). Which call is invalid?

3. Returns, None, and returning several things

Every function call produces a value. If the body never executes a return, or executes a bare return with nothing after it, the value is None, the absence marker you met in Part 1. This explains a classic confusion: print returns None, so x = print("hi") prints the text and leaves x holding None. Printing is something a function does; returning is what it hands back. Keep those separate in your head and a whole category of beginner bugs disappears.

Python functions can also return several values at once by listing them after return, separated by commas. Technically this returns a tuple, which you will formally meet in Part 5, but the calling side reads beautifully either way: you unpack the results into names in one line. Functions that compute two related things, like a minimum and a maximum, or a quotient and a remainder, use this everywhere in real code.

def min_max(numbers):
    lowest = highest = numbers[0]
    for n in numbers:
        if n < lowest:
            lowest = n
        if n > highest:
            highest = n
    return lowest, highest

low, high = min_max([13, 4, 27, 9, 18])
print(f"range: {low} to {high}")    # range: 4 to 27

4. Scope: where names live

Variables assigned inside a function are local: they are created when the call starts, and they vanish when it ends. The function cannot accidentally overwrite your script's variables, and two functions can both use a name like total without colliding. Reading an outer variable from inside a function works, but assigning to one creates a new local instead, which is precisely the behavior that protects you. Python does provide a global keyword to force assignment to module-level names, and the professional advice is blunt: do not. Functions that communicate through parameters and return values can be understood and tested alone; functions that reach out and mutate shared state cannot.

tax_rate = 0.12          # module level, "global" to this file

def price_with_tax(price):
    total = price * (1 + tax_rate)   # reading a global: fine
    return round(total, 2)

print(price_with_tax(1000))   # 1120.0
# print(total)  # NameError: 'total' lives only inside the call

Reading globals does have one legitimate, everyday form: constants. Values that configure your program and never change, a tax rate, a file path, a retry limit, conventionally live at the top of the file in UPPER_SNAKE_CASE, like TAX_RATE = 0.12, and functions read them freely. The capitals are a promise to every reader that nothing reassigns this name, which is why reading them from inside functions is honest while assigning to them is the sin. Most well-kept Python files open with imports, then constants, then functions, a layout your future projects should copy.

5. Flexible signatures: *args and **kwargs

Sometimes a function should accept any number of arguments; print itself is the obvious example. A parameter written *args collects all extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary. You will write these occasionally and read them constantly, because libraries use them to pass options through to lower layers. For now, one honest example is enough to demystify the asterisks forever.

def average(*numbers):
    if not numbers:                  # truthiness, Part 2
        return 0.0
    return sum(numbers) / len(numbers)

print(average(4, 8))           # 6.0
print(average(1, 2, 3, 4, 5))  # 3.0
print(average())               # 0.0

The mirror image of *args is unpacking at the call site, and it completes the picture: if your values already live in a list or a dictionary, a star spreads them into a call. numbers = [4, 8, 15] and then average(*numbers) passes three separate arguments, and options = {"punctuation": "!", "lang": "si"} with make_greeting("Amina", **options) fills keyword parameters from the dict. You will meet this constantly in library code, where functions forward everything they received to another function with a single *args, **kwargs pair.

One more idea belongs in your model of functions before lambdas make sense: in Python, functions are values. A def creates an object, the name is an ordinary variable pointing at it, and you can store functions in lists, pass them as arguments, and return them from other functions. That is not an exotic trick; it is how sorted lets you customize ordering and how half the standard library accepts behavior as a parameter. The lambda syntax below exists purely to make passing a small function less ceremonious.

6. Lambdas: tiny anonymous functions

A lambda is a function with no name and a single expression as its body: lambda x: x * 2 is the throwaway equivalent of a two-line def. Lambdas exist for one main purpose, to be passed as arguments to functions that want a bit of logic, and the canonical example is sorting by a key. sorted accepts a key function that it calls on each item to decide order, and a lambda is the natural way to say sort by the second element or sort by length, inline, at the call site. If a lambda grows beyond one short expression, promote it to a def with a name; readability always wins.

students = [("Amina", 87), ("Zane", 91), ("Ruwan", 78)]

by_score = sorted(students, key=lambda s: s[1], reverse=True)
print(by_score)
# [('Zane', 91), ('Amina', 87), ('Ruwan', 78)]

words = ["banana", "fig", "apple"]
print(sorted(words, key=len))      # ['fig', 'apple', 'banana']

Checkpoint

What does x = print("hello") leave in x?

7. Practice: refactoring a script into functions

The playground below contains the Part 2 tax calculator rewritten as functions, plus a tip calculator with a deliberate bug. Your exercises: fix the bug by reading the traceback, add a default argument, and extract one more function. Refactoring working code into cleaner shape is half of professional programming, and it is a skill you can only build by doing exactly this.

Python playground

Notice the architecture even in this toy: tax_rate_for handles policy, tax_due handles arithmetic, and each can change without touching the other. When Part 13 introduces automated testing, functions like these will be trivially testable precisely because they take inputs and return outputs with no side effects. Good structure now is free testing later.

! Common mistakes to avoid

  • Calling a function before defining it and getting NameError.

    Python reads files top to bottom; def must run before the call. Standard layout: all definitions first, then the code that uses them.

  • Forgetting the parentheses: writing result = my_func instead of my_func().

    Without parentheses you get the function object itself, not its result. If you print something like <function my_func at 0x...>, you forgot to call.

  • Using a list or dict as a default parameter value.

    Defaults are evaluated once at definition time and shared across calls. Default to None and create the fresh object inside the body.

  • Relying on print inside functions instead of return, then being unable to use the result.

    Compute and return in the function; print at the call site. Functions that return are reusable and testable; functions that only print are dead ends.

? Frequently asked questions

How long should a function be? +

Short enough to name honestly. Many professionals aim for a screen or less, but the real test is whether the name describes everything the body does. The moment a function needs "and" in its name, split it.

What is the difference between parameters and arguments? +

Parameters are the names in the def line; arguments are the actual values supplied at the call. The distinction matters when reading error messages, which use both words precisely.

When should I use lambda instead of def? +

Only when passing a one-expression function as an argument, typically as a sort key or a filter. Anything you would call twice, or that needs a second line, deserves a def and a name.

Can functions call themselves? +

Yes, that is recursion, and Python supports it with a default depth limit of about a thousand calls. It is elegant for tree-shaped problems, and you will meet it naturally when we walk folder structures in Part 8.

8. Recap and what comes next

You can now define functions, pass arguments by position and keyword, set safe defaults, return one value or several, reason about local scope, accept variable arguments, document with docstrings, and wield lambdas where they belong. From this point in the course, every example will be organized into functions, because that is how real Python is written, and your scripts have officially become programs.

Next comes the lesson many learners call the moment Python clicks: Part 5, Python data structures, covering lists, tuples, sets, dictionaries, and the comprehensions that make Python famous. The Functions lesson and quiz in the Learn Python app below give you extra reps on today's material, and the whole syllabus is on the series hub.

💡

Pro tip

After writing any function, immediately call it twice with different arguments, including one edge case like zero or an empty string. Thirty seconds of poking finds the bugs that an hour of rereading misses, and it is exactly the mindset that becomes automated testing in Part 13.

Learn Python Android app icon

Practice on the go

Learn Python, the free Android app

Every topic in this series lives in the app too: bite-size lessons, runnable examples, quizzes, mini projects, and an offline Python playground that runs on your phone.

Newsletter

Want more posts like this?

Get practical software notes and tutorials delivered when something new is published.

No spam. Unsubscribe anytime.

How did this land?

Comments

0
Log in or sign up to join the discussion and react to this post.

No comments yet. Be the first to share your thoughts.

Related posts

Important functionalities of Pandas in Python : Tricks and Features

Pandas is one of my favorite libraries in python. It’s very useful to visualize the data in a clean structural manner. Nowadays Pandas is widely used in Data Science, Machine Learning and other areas.

5 years ago

How to get data from twitter using Tweepy in Python?

To start working on Python you need to have Python installed on your PC. If you haven’t installed python. Go to the Python website and get it installed.

6 years ago

Predicting per capita income of the US using linear regression

Python enables us to predict and analyze any given data using Linear regression. Linear Regression is one of the basic machine learning or statistical techniques created to solve complex problems.

6 years ago

Essential Sorting Algorithms for Computer Science Students

Algorithms are commonly taught in Computer Science, Software Engineering subjects at your Bachelors or Masters. Some find it difficult to understand due to memorizing.

6 years ago

GraphQL in Laravel Using Lighthouse

In modern web development, GraphQL has emerged as a powerful alternative to REST APIs due to its flexibility and efficiency.

1 year ago