How to Use Functions in Python: Parameters, Scope & More

A function in Python is a reusable block of code that runs only when you call it. You define one with the def keyword, give it a name, add parentheses for any inputs, and end the line with a colon. Everything indented beneath that line is the function’s body. Here’s the simplest possible example:

def greet():
    print("Hello, world!")

greet()  # prints: Hello, world!

That covers the absolute basics, but functions can do much more. This guide walks through defining functions, passing data into them, getting data back out, and the patterns you’ll use in real projects.

Defining a Function

Every function starts with a header line: the def keyword, the function’s name, a pair of parentheses (with or without parameters), and a colon. The body of the function is everything indented one level below that header. Python uses indentation to mark code blocks rather than curly braces or keywords like “end.”

def calculate_tax(price, rate):
    tax = price * rate
    return tax

A few naming rules: function names must start with a letter or underscore, can contain letters, numbers, and underscores, and are case-sensitive. The Python convention is to use lowercase words separated by underscores, like calculate_tax or send_email.

Parameters and Arguments

Parameters are the variable names inside the parentheses when you define a function. Arguments are the actual values you pass in when you call it. Python gives you several ways to handle these.

Positional Arguments

The most straightforward approach. Arguments are matched to parameters based on their position, left to right:

def subtract(a, b):
    return a - b

subtract(10, 3)   # a=10, b=3, returns 7
subtract(3, 10)   # a=3, b=10, returns -7

Order matters here. Swapping the arguments changes the result.

Keyword Arguments

You can also pass arguments by explicitly naming the parameter. When you do this, order no longer matters:

subtract(b=3, a=10)  # still returns 7

This is especially helpful when a function has many parameters and you want the call to be readable.

Default Values

You can assign a default value to any parameter. If the caller doesn’t provide that argument, the default kicks in:

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

greet("Alice")              # returns "Hello, Alice!"
greet("Alice", "Hey there") # returns "Hey there, Alice!"

Parameters with default values must come after parameters without them. Writing def greet(greeting="Hello", name) would raise a syntax error.

Returning Values

The return keyword sends a value back to the code that called the function and immediately ends the function’s execution. Anything after the return line (at the same indentation level) won’t run.

def divide(a, b):
    if b == 0:
        return "Cannot divide by zero"
    return a / b

result = divide(10, 2)  # result is 5.0

If your function has no return statement, or uses return without a value, Python implicitly returns None. This is fine for functions that perform an action (like printing or writing to a file) rather than computing a result.

You can also return multiple values by separating them with commas. Python packs them into a tuple:

def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([4, 1, 9, 3])  # low=1, high=9

Variable Scope

A variable created inside a function exists only inside that function. This is called local scope. Once the function finishes, the variable is gone:

def set_value():
    x = 10
    print(x)  # works fine

set_value()
print(x)  # NameError: x is not defined

A variable created outside any function has global scope and can be read from inside a function. But if you assign to a variable with the same name inside the function, Python treats it as a new, separate local variable:

x = 50

def show():
    x = 10       # this is a local x, not the global one
    print(x)     # prints 10

show()
print(x)         # prints 50, unchanged

If you genuinely need to modify a global variable from inside a function, you can use the global keyword. In most cases, though, it’s cleaner to pass values in as arguments and get results back via return. Relying on global state makes code harder to debug.

Python looks up variable names in a specific order: local scope first, then any enclosing functions (for nested functions), then the global scope, and finally Python’s built-in names. This is sometimes called the LEGB rule.

Accepting a Variable Number of Arguments

Sometimes you don’t know in advance how many arguments a function will receive. Python handles this with two special operators.

*args for Extra Positional Arguments

Prefixing a parameter with a single asterisk collects any extra positional arguments into a tuple:

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

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

The name args is just a convention. The asterisk is what matters. You could write *numbers and it would work the same way.

**kwargs for Extra Keyword Arguments

A double asterisk collects any extra keyword arguments into a dictionary:

def build_profile(**kwargs):
    return kwargs

build_profile(name="Alice", age=30, city="Denver")
# returns {'name': 'Alice', 'age': 30, 'city': 'Denver'}

You can combine all parameter types in one function. The required order is: regular positional parameters, then *args, then keyword parameters with defaults, then **kwargs:

def example(a, b, *args, flavor="vanilla", **kwargs):
    print(a, b, args, flavor, kwargs)

Lambda Functions

A lambda is a small, anonymous function written on a single line. It can take any number of arguments but contains only one expression:

double = lambda x: x * 2
double(5)  # returns 10

Lambdas are most useful when you need a short function as an argument to another function, like sorting a list of tuples by the second element:

pairs = [(1, "b"), (3, "a"), (2, "c")]
pairs.sort(key=lambda item: item[1])
# pairs is now [(3, 'a'), (1, 'b'), (2, 'c')]

For anything more than a simple expression, a regular def function is easier to read and debug.

Type Hints

Python lets you annotate your function parameters and return values with expected types. These annotations don’t enforce anything at runtime, but they make your code more readable and allow tools like linters and editors to catch bugs early:

def surface_area_of_cube(edge_length: float) -> float:
    return 6 * edge_length ** 2

The syntax is straightforward: place a colon and the type after each parameter, and use -> before the final colon to indicate the return type. For more complex types (a list of integers, an optional string), the typing module provides tools like list[int] and Optional[str].

Putting It All Together

Here’s a more realistic example that combines several concepts: default parameters, a return value, and a clear name that describes what the function does.

def format_price(amount, currency="USD", decimals=2):
    symbol = {"USD": "$", "EUR": "€", "GBP": "£"}.get(currency, currency)
    return f"{symbol}{amount:.{decimals}f}"

format_price(19.5)                  # "$19.50"
format_price(19.5, currency="EUR")  # "€19.50"
format_price(1234, decimals=0)      # "$1234"

Functions become powerful once you start composing them. Small, focused functions that each do one thing are easier to test, reuse, and combine than one massive block of code. A good rule of thumb: if you find yourself copying and pasting the same logic, wrap it in a function and call it from both places.