---
title: Functions in Python in Depth: The Complete Guide
source: https://app.sythra.ai/learn/python/python-functions-in-depth
topic: Python
updated: 2026-08-12
publisher: Sythra (https://app.sythra.ai)
---

# 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.

_Source: [https://app.sythra.ai/learn/python/python-functions-in-depth](https://app.sythra.ai/learn/python/python-functions-in-depth) — free to read on Sythra._

## Key points

- Default arguments make parameters optional — never default to a mutable list or dict
- Keyword arguments let you call a function by naming parameters, so order stops mattering
- *args gathers extra positional arguments into a tuple; **kwargs gathers extra keyword arguments into a dict
- * and ** also unpack a list/dict into positional/keyword arguments when calling a function
- lambda x: expr writes a one-line function, best used inline (e.g. sorted()'s key=)
- Local variables live and die inside their function; global requires the global keyword to modify
- Functions are values — you can store them in variables and pass them into other functions

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.

> **Already know the basics?:** 

## 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:

```python
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](/learn/python/functions-in-python), and everything about `return`, incremental development, and boolean functions lives in [Fruitful Functions in Python](/learn/python/fruitful-functions-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:

```python
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.

> **Rule:** 

### The mutable default argument trap

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

```python
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:

```python
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:

```python
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:

```python
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:

```python
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**:

```python
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:

```python
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 `**`:

```python
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:

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

Compare it to the equivalent regular function:

```python
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:

```python
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.

> **Keep lambdas simple:** 

## 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:

```python
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:

```python
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:

```python
count = 0

def increase():
    global count
    count += 1
```

> **The general rule:** 

## 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:

```python
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

| Tool | What it does | Example |
| --- | --- | --- |
| def | Defines a named, reusable function | def greet(name): |
| return | Hands a value back to the caller | return x * 2 |
| Default argument | Makes a parameter optional | def f(x, y=10): |
| Keyword argument | Names a parameter at call time | f(y=5, x=1) |
| *args | Gathers extra positional args into a tuple | def f(*args): |
| **kwargs | Gathers extra keyword args into a dict | def f(**kwargs): |
| lambda | One-line anonymous function | lambda x: x + 1 |
| global | Lets a function write to a global variable | global 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

- [Functions in Python: Definition and How They Work](/learn/python/functions-in-python) — start here if def, parameters, and the flow of execution are new
- [Fruitful Functions in Python](/learn/python/fruitful-functions-python) — return values, incremental development, boolean functions
- [Built-in Functions in Python](/learn/python/built-in-functions-python) — the functions always available, no import needed
- [Libraries and Modules in Python](/learn/python/libraries-and-modules-python) — bringing in pre-built tools with import
- [Recursion in Python](/learn/python/recursion-in-python) — functions that call themselves

> **Practice with Sythra:**  [Practice with AI tutor](https://app.sythra.ai/pricing)

## FAQ

### 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.

## Related

- [Functions in Python: Definition and How They Work](https://app.sythra.ai/learn/python/functions-in-python) — Start here: def, parameters, and the flow of execution.
- [Fruitful Functions in Python](https://app.sythra.ai/learn/python/fruitful-functions-python) — Return values, incremental development, boolean functions.
- [Built-in Functions in Python](https://app.sythra.ai/learn/python/built-in-functions-python) — type(), len(), range(), and more — no import needed.
- [Libraries and Modules in Python](https://app.sythra.ai/learn/python/libraries-and-modules-python) — Bringing in pre-built tools with import.
- [Python course hub](https://app.sythra.ai/learn/python) — All free Python explainers and the path into Agentic practice.
- [Recursion in Python](https://app.sythra.ai/learn/python/recursion-in-python) — Base cases, stack frames, factorial, and Fibonacci.
- [The match-case Statement in Python (Python's Switch)](https://app.sythra.ai/learn/python/match-case-python) — match/case, default cases, and dictionary dispatch.
- [Classes and Functions in Python (Pure Functions vs. Modifiers)](https://app.sythra.ai/learn/python/classes-and-functions-python) — Pure functions, modifiers, and planned development.

---

Written by Sythra — Learn machine learning by building. Practice this topic with Sythra's AI tutor: https://app.sythra.ai/pricing
