Defining and Calling Functions
A function packages a piece of work behind a name. def creates it, the call runs it, and return decides what comes back - which is None if you never say.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
Defining a function
def greet(name):
"""Return a greeting for the given name."""
return f"Hello, {name}"
message = greet("Meera")
print(message) # Hello, Meera| Part | Meaning |
|---|---|
def | The keyword that creates a function object. |
greet | The name the function object is bound to. |
(name) | The parameter list. These are local names, filled in at call time. |
: | Opens the body. |
| the docstring | Optional, but the first thing anyone reads. |
return | Ends the function and hands a value back. |
defis a statement that runs. It creates a function object and binds a name to it. Calling a function before thedefhas executed is aNameError, which is why functions are defined before the code that uses them.
Parameters and arguments
def rectangle_area(width, height): # width and height are PARAMETERS
return width * height
print(rectangle_area(4, 5)) # 4 and 5 are ARGUMENTSParameters are the names in the definition. Arguments are the values supplied at the call. The distinction matters when reading error messages.
return
def add(a, b):
return a + b
def show(a, b):
print(a + b) # prints, but returns nothing
result_1 = add(2, 3)
result_2 = show(2, 3)
print(result_1) # 5
print(result_2) # NoneA function with no return, or with a bare return, gives back None. Printing and returning are different actions: printing sends text to the screen, returning hands a value to the caller. A function that only prints cannot be reused in a calculation.
return exits immediately
def classify(n):
if n < 0:
return "negative"
if n == 0:
return "zero"
return "positive"
print("never runs") # unreachable
print(classify(-5)) # negativeReturning several values
def statistics(values):
return min(values), max(values), sum(values) / len(values)
low, high, mean = statistics([4, 8, 15])
print(low, high, mean) # 4 15 9.0That is one tuple, unpacked at the call site.
Docstrings
def net_price(amount, tax_rate=0.18):
"""Return the amount including tax.
Args:
amount: The pre-tax amount.
tax_rate: The tax rate as a fraction. Defaults to 0.18.
Returns:
The amount plus tax, as a float.
"""
return amount * (1 + tax_rate)
print(net_price.__doc__)
help(net_price)A docstring is a string literal as the first statement of the function. Unlike a comment it is kept at runtime, which is what makes help() and editor tooltips work. Write one for every function that anyone else will call.
Functions are objects
def double(n):
return n * 2
print(type(double)) # <class 'function'>
print(double.__name__) # double
twice = double # a second name for the same function
print(twice(5)) # 10
operations = [double, abs, len]
print(operations[0](7)) # 14
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]A function can be stored in a variable, put in a list, passed to another function and returned from one. This is what "first class" means, and the advanced functions note builds on it.
How arguments are passed
def rebind(items):
items = [9, 9] # rebinds the LOCAL name only
return items
def mutate(items):
items.append(9) # changes the object the caller passed
original = [1, 2]
rebind(original)
print(original) # [1, 2] - unchanged
mutate(original)
print(original) # [1, 2, 9] - changedPython passes the reference by value. The function gets its own name pointing at the caller's object. Rebinding that name affects nothing outside; mutating the object it points at affects everyone. This is neither "by value" nor "by reference" in the classical sense, and remembering the two examples above is more useful than remembering a label for it.
How to avoid surprising the caller
def add_tax(prices):
return [p * 1.18 for p in prices] # returns a new list
def add_tax_in_place(prices):
for i in range(len(prices)):
prices[i] *= 1.18 # changes the caller's listPrefer the first form. A function that returns a new value is easier to test, easier to reuse and impossible to misuse by accident. Mutate the argument only when that is the documented purpose of the function.
Writing good functions
# Does too much
def process(data):
cleaned = [d.strip().lower() for d in data if d.strip()]
counts = {}
for item in cleaned:
counts[item] = counts.get(item, 0) + 1
top = sorted(counts.items(), key=lambda p: -p[1])[:3]
print("Top three:")
for word, count in top:
print(f" {word}: {count}")
return top# One job each
def clean(data):
return [d.strip().lower() for d in data if d.strip()]
def count_items(items):
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
return counts
def top_n(counts, n=3):
return sorted(counts.items(), key=lambda p: -p[1])[:n]
def report(top):
print("Top three:")
for word, count in top:
print(f" {word}: {count}")
words = clean([" Apple ", "apple", "Fig", "", "fig", "fig"])
report(top_n(count_items(words)))Each piece can now be tested alone, and only the last one touches the screen. Keeping input, computation and output in separate functions is the most valuable habit in this note.
Type hints
def net_price(amount: float, tax_rate: float = 0.18) -> float:
return amount * (1 + tax_rate)
print(net_price(100)) # 118.0
print(net_price("abc")) # still runs, then fails insideAnnotations are documentation that tools can read. Python does not enforce them at runtime; a separate type checker does. The type hints note covers this in full.
Common mistakes
- Forgetting
returnand wondering why the caller hasNone. - Using
printwherereturnwas needed, making the function unusable in a calculation. - Calling a function before its
defhas run. - Mutating an argument the caller did not expect to be changed.
- Writing a function that does four things, so it can never be reused.
- Shadowing a built in: naming a function
list,sumorinput.
Best practices
- One job per function, and a name that says what that job is.
- Return values; print only in the functions whose purpose is output.
- Write a docstring for anything another person will call.
- Prefer returning a new object over modifying an argument.
- Keep a function short enough to read without scrolling.
Practice
- Write a function that returns the average of a list and handles the empty list case explicitly.
- Show two functions that take a list, one that mutates it and one that does not, and prove the difference.
- Split a function that reads input, computes and prints into three functions.
- Explain why
print(f())showsNonefor a function that itself prints. - Write a documented function with a default argument and read its docstring at runtime.
Conclusion
A function is an object created by def, called by name, and defined by what it returns. Give each one a single job, return rather than print, and be deliberate about whether it changes the arguments it is given.