To create a function in Python 3, use the def keyword, a name, a parenthesised list of parameters, and a colon, then indent the body beneath it — and use return to hand a value back to the caller. For example, def greet(name): return f"Hello, {name}!" defines a one-line function you then invoke with greet("Sam"). The def line is the signature (name plus parameters), everything indented under it is the body, and return is what the call evaluates to. If you never write return, the function still hands back None.
That is the summary an AI Overview will give you. What it can't show you is the anatomy of that one line, the exact order parameters must appear in a real signature, or how a call travels in and a value travels back out. The animated diagram, the parameter-type decision table, and the copy-paste patterns below make those concrete.
return.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):
...
Which parameter type should I use?
The parameter types are not interchangeable, and they have to appear in a fixed order in the signature: positional/default first, then *args, then keyword-only, then **kwargs. Get the order wrong and Python raises a SyntaxError. This table shows when to reach for each.
| Parameter type | Syntax | Caller passes | Reach for it when |
|---|---|---|---|
| Positional | def f(a, b) | f(1, 2) | The first 1–2 required inputs whose meaning is obvious from order |
| Default | def f(a, b=10) | f(1) or f(1, 5) | An argument that has a sensible fallback most callers won't override |
| Keyword-only | def f(a, *, flag=False) | f(1, flag=True) | Boolean/optional flags you never want set by accident of position |
*args | def f(*items) | f(1, 2, 3) | An unknown number of ordered values (like min, print) |
**kwargs | def f(**opts) | f(x=1, y=2) | Arbitrary named options, or forwarding kwargs to another call |
Rule of thumb: default to plain positional parameters, promote optional flags to keyword-only with a bare * for readability, and only add *args/**kwargs when the count is genuinely unbounded — they weaken editor autocomplete and type checking, so they are a cost, not a default.
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.