Python22 min readBeginner

Functions & Scope

Wrap code in reusable, named units. Default args, *args, **kwargs, scope rules.

Why functions exist

A function is a named, reusable block of code. The two reasons functions exist: first, to avoid repeating yourself (DRY — Don't Repeat Yourself). Second, to give a name to a chunk of behavior so the rest of your code reads more like English.

If you find yourself copy-pasting the same five lines, that's the signal to extract a function.

Defining and calling a function

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

message = greet("Ada")
print(message)

Anatomy of that definition: `def` is the keyword that starts a function definition. `greet` is the function name. `(name)` lists the parameters — placeholders for the values the caller will pass in. The colon ends the header. Everything indented underneath is the function body. `return` sends a value back to whoever called the function.

If you don't write `return`, the function automatically returns the special value `None`.

Default arguments

You can give parameters default values. If the caller doesn't supply that argument, Python uses the default.

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

print(greet())          # Hello, world!
print(greet("Ada"))    # Hello, Ada!
⚠ Watch out
Never use a mutable object (like a list or dict) as a default argument. The default is created once at function-definition time, not on every call, and gets shared across calls — a classic source of bugs. Use `None` and create the list inside the function instead.

Keyword arguments

When you call a function, you can pass arguments by name. This makes calls self-documenting and lets you skip optional arguments in the middle.

def book_flight(origin, destination, seat="economy", refundable=False):
    return f"{origin} → {destination}, {seat}, refundable={refundable}"

print(book_flight("LAX", "NYC", refundable=True))

*args and **kwargs

Sometimes a function needs to accept a variable number of arguments. `*args` collects extra positional arguments into a tuple; `**kwargs` collects extra keyword arguments into a dict.

def total(*nums, label="sum"):
    return f"{label}: {sum(nums)}"

print(total(1, 2, 3))                       # sum: 6
print(total(1, 2, 3, 4, label="result"))    # result: 10

def describe(**props):
    for key, value in props.items():
        print(f"{key} = {value}")

describe(name="Ada", age=36, role="engineer")

Type hints

Python is dynamically typed, but you can annotate parameters and return values with their expected types. The interpreter doesn't enforce these at runtime — they are documentation that tools like mypy and your IDE can check. They're optional, but on any non-trivial codebase they pay for themselves.

def discount(price: float, percent: int = 10) -> float:
    return price * (1 - percent / 100)

print(discount(100.0, 25))   # 75.0

Scope: where variables live

Variables defined inside a function are local — they exist only while the function is running and don't leak out. Variables defined at the top level of a module are global. When Python looks up a name, it checks scopes in this order: Local → Enclosing → Global → Built-in (the LEGB rule).

x = 10            # global

def show():
    x = 99        # local — does NOT change the global
    print("inside:", x)

show()
print("outside:", x)

If you really need to modify a global from inside a function, use the `global` keyword — but most of the time, that's a sign you should be passing the value in and returning the new one instead.