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!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.0Scope: 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.