Functions are Python's primary unit of code organization. They name a block of statements, accept inputs as parameters, and return a value. This lesson covers every form of argument, the return statement, type hints, and the scope rules that decide what a name refers to.
def and return
A function starts with the def keyword, a name, a parameter list in parentheses, a colon, and an indented body. The return statement sends a value back to the caller. A function with no return statement returns None.
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("Asha")) # Hello, Asha!
def log(message):
print(message)
# no return, returns None implicitly
result = log("hi")
print(result is None) # True
Positional, keyword, and default arguments
Arguments are passed either by position or by keyword. You can mix them, but positional arguments must come before keyword arguments. Default values are evaluated once when the function is defined.
def make_coffee(size, drink="espresso", sugar=0):
return f"{size} {drink}, {sugar} sugar"
print(make_coffee("large")) # large espresso, 0 sugar
print(make_coffee("medium", "latte")) # medium latte, 0 sugar
print(make_coffee("small", sugar=2)) # small espresso, 2 sugar
print(make_coffee("small", drink="cappuccino", sugar=1)) # small cappuccino, 1 sugar
*args and **kwargs
Use *args to capture extra positional arguments as a tuple, and **kwargs to capture extra keyword arguments as a dict. The names are convention; the syntax is what matters. Useful when you want to wrap another function or build a flexible API.
def log_call(func, *args, **kwargs):
print(f"calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
def add(a, b, *, multiplier=1):
return (a + b) * multiplier
print(log_call(add, 3, 4, multiplier=2)) # 14
Keyword-only and positional-only arguments
Arguments after a * are keyword-only. Arguments before a / are positional-only. Use these markers to make the call site clearer and to protect your function's API from accidental positional misuse.
def set_size(width, height, /, *, unit="px"):
return f"{width}x{height} {unit}"
print(set_size(1920, 1080)) # 1920x1080 px
print(set_size(1920, 1080, unit="cm")) # 1920x1080 cm
# set_size(width=1920, height=1080) # TypeError: width is positional-only
Scope and the LEGB rule
Python looks up names in four scopes, in this order: Local, Enclosing, Global, Built-in. This is the LEGB rule. Understanding it explains why a variable in one function does not leak into another, and how closures capture enclosing variables.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
global and nonlocal
Use global to rebind a module-level name from inside a function. Use nonlocal to rebind a name in an enclosing (but not global) scope. Both are rare. Prefer returning a new value over mutating a shared name.
counter = 0
def tick():
global counter
counter += 1
tick(); tick(); tick()
print(counter) # 3
Docstrings and type hints
Document a function with a triple-quoted string right after the def line. Add type hints to parameters and the return value for editor support and clearer intent. They do not enforce types at runtime but make code much easier to read and refactor.
def word_count(text: str) -> dict[str, int]:
"""Return a mapping of word to occurrence count for the given text."""
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
print(word_count("the quick brown fox jumps over the lazy dog the"))
Common pitfalls when defining functions
Three issues show up over and over in real code. First, mutable default arguments are evaluated once at function-definition time, so def add(item, bucket=[]): appends to the same list on every call. Default to None and create the list inside the body: def add(item, bucket=None): bucket = [] if bucket is None else bucket. Second, late binding in closures catches newcomers building a list of callbacks in a loop. Each lambda in [lambda: i for i in range(3)] captures the variable i, not its current value, so all three callbacks print 2. Bind the value as a default: [lambda i=i: i for i in range(3)]. Third, returning a mutable default also shares state across calls, so a module-level cache dict that you mutate instead of returning a fresh value will accumulate surprises across tests.
Comparison with JavaScript and C++ functions
Python's def is a statement that creates a function value and binds it to a name. JavaScript's function declaration is similar in shape, but arrow functions behave like Python lambdas: they close over variables, do not bind their own this, and are expressions rather than statements. C++ free functions are closer to def than to either, but with several differences: C++ functions require explicit return types at the point of declaration, support overloading by argument type, and can be templated. Python overloads through *args, **kwargs, and libraries like functools.singledispatch instead. If you come from C++ and miss overloading, look at singledispatch before reaching for a hand-written isinstance ladder.
Best practices for function design in 2026
Three habits keep a function library maintainable. First, keep functions short. The sweet spot is roughly five to twenty lines; if a function grows past thirty, look for refactoring with extract-function and parameter object patterns. Second, prefer pure functions wherever possible: same input, same output, no side effects. Pure functions are easier to test, easier to parallelise, and easier to memoise. Third, annotate every public function with type hints and a docstring, even when the body is one line. The hints drive IDE autocompletion, mypy, and runtime helpers like dataclasses; the docstring is what users see on hover and on help(). Together they turn a function from "code I have to read" into "a tool I can call".
Summary
Use def to name behaviour. Use positional arguments for required inputs, keyword arguments for optional ones, *args and **kwargs for flexible forwarding. Type hints and docstrings make functions self-documenting. The next lesson covers lambdas, comprehensions, decorators, and generators.