SythraOpen app

Functions in Python in Depth: The Complete Guide

Beyond basic def and return, Python functions support default arguments, keyword arguments, *args and **kwargs for unlimited inputs, one-line lambda functions, and strict local/global variable scope rules — and functions themselves can be passed around like any other value.

Sythra

16 min read

XLinkedIn
Functions in Python in Depth: The Complete Guide — cover illustration

This is the complete guide to functions in Python — everything from your very first def to the patterns real codebases use every day: default arguments, keyword arguments, *args, **kwargs, lambdas, and variable scope. If you've read the other function pages on Sythra, consider this the page that ties them all together in one place.

What you will learn

  • A full recap: defining functions, parameters, arguments, and return values
  • Default arguments — and the famous mutable-default trap
  • Keyword arguments and why they make calls more readable
  • *args and **kwargs — accepting any number of inputs
  • Unpacking a list or dict into a function call with * and **
  • Lambda functions — tiny, throwaway, single-expression functions
  • Variable scope — what a function can and cannot see
  • Functions as values — passing a function into another function

Quick recap: what a function is

A function is a named, reusable block of code, defined once with def and called by name as many times as you like:

def greet(name):
    return "Hello, " + name + "!"

print(greet("Asha"))   # Hello, Asha!

name is a parameter — the placeholder inside the definition. "Asha" is the argument — the actual value passed in at call time. return hands a value back to the caller; without it, the function returns None. The full walkthrough — including local scope basics and the flow of execution — lives in Functions in Python: Definition and How They Work, and everything about return, incremental development, and boolean functions lives in Fruitful Functions in Python. This page assumes you already have that foundation and goes further.

Default arguments: making parameters optional

You can give a parameter a default value, which gets used automatically if the caller doesn't supply one:

def greet(name, greeting="Hello"):
    print(greeting + ", " + name + "!")

greet("Asha")                    # Hello, Asha!
greet("Ravi", "Good morning")    # Good morning, Ravi!

If greeting isn't provided, Python quietly falls back to "Hello". If it is provided, that value overrides the default completely.

The mutable default argument trap

Never use a mutable object — a list or dictionary — as a default value:

def add_item(item, basket=[]):    # DANGER
    basket.append(item)
    return basket

print(add_item("apple"))    # ['apple']
print(add_item("banana"))   # ['apple', 'banana']  <- wait, what?!

The default list [] is only created once, the moment the function is defined — and it's quietly reused on every single call that doesn't supply its own list. The fix is to default to None, and create a fresh list inside the function body instead:

def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

This is one of the most famous "gotchas" in all of Python — worth burning into memory now, before it bites you in a real project.

Keyword arguments: naming what you're passing

So far you've called functions by position — the first value goes to the first parameter, and so on. You can instead call them by explicitly naming which parameter each value goes to:

def describe(name, age, city):
    print(name, "is", age, "and lives in", city)

describe(name="Mei", age=28, city="Tokyo")
describe(city="Tokyo", age=28, name="Mei")   # exact same result — order doesn't matter!

When you use keyword arguments, order stops mattering, because you're telling Python exactly which value belongs to which parameter by name. You can also mix positional and keyword arguments, as long as positional ones come first:

describe("Mei", city="Tokyo", age=28)   # works fine

Keyword arguments make function calls dramatically more readable, especially once a function has several parameters that aren't obviously self-explanatory from context.

*args: gathering any number of positional arguments

*args lets a function accept any number of positional arguments, gathered together into a tuple:

def total(*numbers):
    return sum(numbers)

total(1, 2, 3)         # 6
total(10, 20, 30, 40)  # 100

numbers inside the function is just a regular tuple — you can loop over it, index into it, anything you'd do with any other tuple.

**kwargs: gathering any number of keyword arguments

There's a sibling to *args for keyword arguments — **kwargs — which gathers any number of named arguments into a dictionary:

def describe_person(**info):
    for key, value in info.items():
        print(key, "-", value)

describe_person(name="Tom", age=25, city="Mumbai")
# name - Tom
# age - 25
# city - Mumbai

Whatever names the caller used for their keyword arguments become the keys of the info dictionary, automatically. This is genuinely useful when you don't know in advance exactly what information you'll be given, or how much of it.

You'll very often see *args and **kwargs used together, to write a function that genuinely accepts anything at all:

def show_everything(*args, **kwargs):
    print("Positional:", args)
    print("Keyword:", kwargs)

show_everything(1, 2, name="Asha", age=25)
# Positional: (1, 2)
# Keyword: {'name': 'Asha', 'age': 25}

The order matters in the function definition: regular parameters first, then *args, then **kwargs, in that exact order.

Unpacking arguments when calling a function

You can go the other direction too — take a list or tuple and "scatter" it into positional arguments using *, or take a dictionary and scatter it into keyword arguments using **:

def describe(name, age, city):
    print(name, age, city)

info = ("Mei", 28, "Tokyo")
describe(*info)   # same as describe("Mei", 28, "Tokyo")

info_dict = {"name": "Mei", "age": 28, "city": "Tokyo"}
describe(**info_dict)   # same as describe(name="Mei", age=28, city="Tokyo")

This is extremely common when your data is already sitting in a list or dictionary, and you want to feed it straight into a function without manually pulling each piece out yourself.

Lambda functions: tiny, throwaway functions

A lambda is a small, unnamed function, written in a single line, for situations where writing a full def would feel like overkill:

square = lambda x: x ** 2
print(square(5))    # 25

Compare it to the equivalent regular function:

def square(x):
    return x ** 2

Same thing — lambda just skips the def, the function name, and the return keyword, and packs everything into one line. The biggest real use of lambdas is as a quick, disposable function passed directly into something else, especially sorted()'s key argument:

words = ['banana', 'fig', 'kiwi', 'cherry']
sorted_by_length = sorted(words, key=lambda word: len(word))
print(sorted_by_length)
# ['fig', 'kiwi', 'banana', 'cherry']

Here, key=lambda word: len(word) tells sorted() exactly what to sort by — the length of each word — without needing to write and name a whole separate function just for this one-time use.

Variable scope: what can a function actually see?

Scope refers to where in your program a particular variable can be accessed. A variable created inside a function is called a local variable. It only exists while that function is running, and it's completely invisible to any code outside that function:

def my_function():
    x = 10
    print(x)    # 10, fine, x exists here

my_function()
print(x)    # NameError: name 'x' is not defined

x was born inside my_function, lived its entire life inside my_function, and was thrown away the moment the function finished running. Outside the function, it simply never existed.

A variable created outside any function — at the very top level of your program — is a global variable, and any function can freely read it:

greeting = "Hello"

def say_hi():
    print(greeting)    # works fine — reading a global is always allowed

say_hi()    # Hello

But writing to a global from inside a function requires the global keyword, or Python will quietly create a brand-new local variable instead:

count = 0

def increase():
    global count
    count += 1

Functions as values

In Python, functions themselves are values, just like numbers or strings. You can store a function in a variable, pass it as an argument to another function, or even return one from inside another function:

def shout(text):
    return text.upper() + "!"

def whisper(text):
    return text.lower() + "..."

def greet(func, name):
    print(func(name))

greet(shout, "hello")     # HELLO!
greet(whisper, "hello")   # hello...

greet doesn't know or care what func actually does — it just calls whatever function gets handed to it. This is a genuinely powerful idea, and it's exactly what makes things like sorted(words, key=lambda w: len(w)) from earlier possible — you're literally handing a function over as a piece of data.

Quick reference: every function tool on one page

ToolWhat it doesExample
defDefines a named, reusable functiondef greet(name):
returnHands a value back to the callerreturn x * 2
Default argumentMakes a parameter optionaldef f(x, y=10):
Keyword argumentNames a parameter at call timef(y=5, x=1)
*argsGathers extra positional args into a tupledef f(*args):
**kwargsGathers extra keyword args into a dictdef f(**kwargs):
lambdaOne-line anonymous functionlambda x: x + 1
globalLets a function write to a global variableglobal count

Common mistakes

  • Using a mutable object (list or dict) as a default argument value
  • Putting a default-value parameter before a non-default one, causing a SyntaxError
  • Forgetting the order rule: regular params, then *args, then **kwargs
  • Writing a multi-line lambda by forcing complicated logic into it instead of using def
  • Trying to modify a global variable inside a function without the global keyword
  • Assuming a local variable from one function is visible inside another

Related pages on functions

Common questions

What is a default argument in Python?

A default argument is a value a parameter uses automatically if the caller doesn't supply one, defined like def greet(name, greeting="Hello"). Default-value parameters must come after parameters without defaults.

Why shouldn't I use a list as a default argument value?

Default argument values are created once, when the function is defined, not on every call. A mutable default like basket=[] gets shared and silently mutated across every call that doesn't pass its own list. Use basket=None and create the list inside the function instead.

What is the difference between *args and **kwargs?

*args collects any number of extra positional arguments into a tuple. **kwargs collects any number of extra keyword arguments into a dictionary. Both let a function accept a flexible, unknown number of inputs.

What is a lambda function in Python?

A lambda is a small, unnamed, single-expression function, written as lambda arguments: expression. It's most often used inline, such as sorted(words, key=lambda w: len(w)), where writing a full def would be overkill.

What is variable scope in Python?

Scope determines where a variable can be accessed. A local variable exists only inside the function that created it. A global variable, defined at the top level, can be read by any function, but writing to it from inside a function requires the global keyword.

Can you pass a function as an argument to another function in Python?

Yes. Functions are values in Python, so you can store them in variables, pass them as arguments, and even return them from other functions — this is what makes sorted(list, key=some_function) work.

Explore

Related topics

Keep going — these sit next to this concept in a real learning path.

Browse all python explainers →