Why Functions Matter in Python 3
Functions let you group logic into reusable, testable units. They reduce duplication, clarify intent, and make it easier to evolve code as requirements change. Python 3 treats functions as first-class objects, meaning you can pass them around, assign them to variables, and even define functions inside other functions.
Defining Your First Function
Create a function with the def keyword, a name, optional parameters, and an indented body:
def greet(name):
message = f"Hello, {name}!"
return message
Call the function by using its name followed by parentheses:
>>> greet("Sam")
'Hello, Sam!'
If you do not explicitly return a value, Python returns None.
Parameters and Arguments
Python functions support several parameter types:
- Positional parameters: standard arguments defined in order.
- Keyword parameters: arguments supplied by name for readability.
- Default parameters: provide fallback values.
- Variadic parameters:
*argscollects extra positional arguments,**kwargscollects keyword arguments.
def log_event(message, level="INFO", *tags, **meta):
print(level, message, tags, meta)
log_event("Signed in", "INFO", "auth", user="sarah")
# Output: INFO Signed in ('auth',) {'user': 'sarah'}
Use keyword-only parameters by placing a * in the signature:
def create_user(username, *, is_admin=False):
...
Documenting Functions
Docstrings describe the intent, parameters, and return values. They power developer tools such as help() and IDE hints:
def convert_to_celsius(fahrenheit: float) -> float:
"""Convert a Fahrenheit temperature to Celsius."""
return (fahrenheit - 32) * 5 / 9
Combine docstrings with type hints to communicate expectations without enforcing them at runtime.
Managing Scope and State
Variables defined inside a function are local and disappear once the function returns. To modify a global variable inside a function, declare it with global, though a better pattern is to return values or wrap state in classes.
Closures (functions defined inside other functions) remember the outer scope:
def multiplier(factor):
def inner(value):
return value * factor
return inner
double = multiplier(2)
double(10) # 20
Handling Errors and Edge Cases
Use raise to report invalid input and try/except blocks where the caller must recover gracefully:
def divide(numerator, denominator):
if denominator == 0:
raise ValueError("Denominator cannot be zero")
return numerator / denominator
Design functions with predictable behavior—validate inputs, avoid hidden side effects, and document any exceptions that may propagate.
Testing Your Functions
Start with simple assertions or use pytest/unittest for structured testing:
def test_convert_to_celsius():
assert convert_to_celsius(212) == 100
assert round(convert_to_celsius(32), 2) == 0
Automated tests catch regressions when you refactor or optimize logic. Pair tests with static analysis tools (e.g., mypy, ruff) to enforce consistent signatures and type usage.
Best Practices Checklist
- Keep functions focused. Aim for single-responsibility behavior.
- Name functions clearly. Choose verbs for actions (
send_email) and nouns for factories (user_from_row). - Limit the parameter count. If signatures grow too large, pass a dataclass or configuration object.
- Avoid mutable default arguments. Use
Nonesentinels and initialize inside the function. - Return explicit results. Prefer returning values over mutating global state.
- Add docstrings and type hints. Make the interface self-documenting for teammates and tooling.
Functions are the building blocks of maintainable Python. By mastering signatures, docstrings, error handling, and test coverage, you establish a foundation that scales from quick scripts to production-grade applications.

