Previous | Tutorial index | Next
In Python, functions are first‑class objects. This means they are treated like any other object: integers, strings, lists, or dictionaries. You can assign them to variables, pass them as arguments to other functions, return them from functions, and store them in data structures. This powerful paradigm enables functional programming techniques, elegant design patterns, and highly reusable code. In this tutorial, we will explore what it means for a function to be a first‑class citizen, how to leverage this in practice, and common pitfalls to avoid.
A first‑class object (or first‑class citizen) in a programming language is an entity that can be:
In Python, functions meet all these criteria. This is not the case in all languages; for example, in C, functions are not first‑class (you can have function pointers, but they are limited).
You can assign a function to a variable, just like any other object. The variable then becomes an alias for the function.
def greet(name):
return f"Hello, {name}"
say_hello = greet
print(say_hello("Alice")) # Hello, Alice
This is useful for renaming functions, storing them for later use, or passing them around.
Functions can be elements of lists, tuples, dictionaries, sets, etc.
operations = {
'add': lambda a, b: a + b,
'sub': lambda a, b: a - b,
'mul': lambda a, b: a * b,
}
print(operations['add'](5, 3)) # 8
You can also have a list of functions to apply sequentially.
This is the foundation of higher‑order functions like map, filter, and sorted. You can pass your own functions to control behavior.
def apply_operation(func, x, y):
return func(x, y)
def multiply(a, b):
return a * b
result = apply_operation(multiply, 4, 5) # 20
A function can create and return another function. This is how closures and decorators work.
def make_multiplier(factor):
def multiplier(x):
return x * factor
return multiplier
times3 = make_multiplier(3)
print(times3(10)) # 30
Functions can be compared for identity (using is) and equality (using ==). Two functions are equal if they are the same object; otherwise they are considered different even if they have the same behavior.
def f(): pass
g = f
print(f is g) # True
h = lambda: None
print(f == h) # False (different objects)
__name__, __doc__, and MoreEvery function object has several attributes that provide metadata.
| Attribute | Description |
|---|---|
__name__ |
The function’s name as a string. |
__doc__ |
The docstring (if any). |
__module__ |
The name of the module in which the function was defined. |
__defaults__ |
A tuple of default argument values. |
__closure__ |
A tuple of cell objects for captured variables (if it’s a closure). |
Example:
def add(a, b=2):
"""Return a + b."""
return a + b
print(add.__name__) # 'add'
print(add.__doc__) # 'Return a + b.'
print(add.__defaults__) # (2,)
These attributes are useful for debugging, introspection, and building tools (like decorators that want to preserve metadata—hence the use of functools.wraps).
You can pass a function to encapsulate an algorithm, allowing the caller to choose the behavior.
def sort_list(lst, strategy):
return strategy(lst)
# Different strategies
def ascending(lst): return sorted(lst)
def descending(lst): return sorted(lst, reverse=True)
def no_sort(lst): return lst
data = [3, 1, 4, 2]
print(sort_list(data, ascending)) # [1, 2, 3, 4]
print(sort_list(data, descending)) # [4, 3, 2, 1]
Callbacks are functions that are invoked when an event occurs. GUI libraries and async code rely heavily on this.
def on_click(event):
print(f"Button clicked at {event.x}, {event.y}")
# Simulated event registration
register_callback('click', on_click)
You can maintain a dictionary (registry) of functions keyed by names, allowing dynamic dispatch.
command_registry = {}
def register_command(name):
def decorator(func):
command_registry[name] = func
return func
return decorator
@register_command('start')
def start_server():
print("Server started")
@register_command('stop')
def stop_server():
print("Server stopped")
# Later, run command by name
command = input("Enter command: ")
if command in command_registry:
command_registry[command]()
Passing functions allows you to inject dependencies or configure behavior without hard‑coding.
def process_data(data, processor):
return [processor(item) for item in data]
# Different processors can be passed
print(process_data([1,2,3], lambda x: x*2)) # [2,4,6]
map, filter, reducesorted with keymax, min with keyWhen storing functions in data structures or using them as defaults, be careful with mutable defaults.
lambda for ReadabilityWhile lambdas are convenient, overusing them can hurt readability. Use named functions for complex logic.
If you wrap a function (e.g., in a decorator), remember to preserve its attributes using functools.wraps.
It’s easy to accidentally pass a function reference when you intend to call it. Example: list(map(str.upper, words)) works, but list(map(str.upper(), words)) would fail. Double‑check your parentheses.
When using functions as keys in a dictionary, they are compared by identity, not by their source code. So two functions with identical code are considered different. This is usually fine.
inspect ModuleThe inspect module provides more advanced introspection capabilities:
import inspect
def f(a, b=1, *args, **kwargs):
pass
sig = inspect.signature(f)
print(sig.parameters) # OrderedDict of parameters
This is useful for building frameworks and libraries.
Python supports function annotations (PEP 3107). They are stored in __annotations__ and can be used for documentation or static analysis.
def greet(name: str) -> str:
return f"Hello, {name}"
print(greet.__annotations__) # {'name': <class 'str'>, 'return': <class 'str'>}
Annotations do not affect runtime behavior but are valuable for code clarity.
We have already covered map, filter, reduce, sorted, etc. Consider writing your own higher‑order functions to encapsulate common patterns.
Example: a retry function that takes a function and retries it upon failure.
def retry(func, max_attempts=3):
for attempt in range(max_attempts):
try:
return func()
except Exception as e:
print(f"Attempt {attempt+1} failed: {e}")
if attempt == max_attempts - 1:
raise
What does it mean for a function to be a first‑class object in Python?
@.Which of the following is NOT a valid way to treat a function as a first‑class object?
def f(): pass; a = fdef f(): pass; list_of_funcs = [f]def f(): pass; return fdef f(): pass; f()What attribute stores a function’s name?
__doc____name____module____defaults__Given def add(a, b): return a + b, what is add.__defaults__?
None()(a, b)(b,)What is the output of the following code?
def make_greeting(greeting):
def greet(name):
return f"{greeting}, {name}"
return greet
hello = make_greeting("Hi")
print(hello("Alice"))
Hi, AliceAlice, Himake_greeting returnedErrorTrue or False: Two functions with the same code but defined separately are considered equal (==).
Which module provides tools for advanced function inspection?
sysinspectfunctoolstypesWhat is the result of sorted([1,2,3], key=lambda x: -x)?
[1,2,3][3,2,1][-1,-2,-3][1,2,3] (no change)What does the __annotations__ attribute contain?
When passing a function to another function, what is the receiving function called?
Exercise 1: Function Registry
Create a registry that allows registering and running functions by name.
def register(name, func): registry[name] = func
def run(name, *args, **kwargs): if name not in registry: raise KeyError(f"Function {name} not found") return registryname
register("add", lambda a,b: a+b) register("mul", lambda a,b: a*b) print(run("add", 3, 4)) # 7 print(run("mul", 3, 4)) # 12
</details>
**Exercise 2: Map with Multiple Functions**
Write `apply_all(funcs, iterable)` that applies all functions in sequence to each element.
<details><summary>Sample Solution</summary>
```python
from functools import reduce
def apply_all(funcs, iterable):
return [reduce(lambda val, f: f(val), funcs, item) for item in iterable]
funcs = [lambda x: x+1, lambda x: x*2]
print(apply_all(funcs, [1,2,3])) # [4,6,8]
Exercise 3: Function Composition
Write compose(*funcs) that composes functions from right to left.
def add1(x): return x+1 def square(x): return x*x
h = compose(add1, square) # add1(square(x)) print(h(3)) # 10
</details>
**Exercise 4: Cache Decorator (Using First‑Class Functions)**
Write a function `cache_results(func)` that returns a wrapper with a `cache` attribute.
<details><summary>Sample Solution</summary>
```python
def cache_results(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
wrapper.cache = cache
return wrapper
@cache_results
def slow_square(x):
import time
time.sleep(1)
return x*x
print(slow_square(5)) # takes 1s
print(slow_square(5)) # instant
print(slow_square.cache) # {(5,): 25}
Exercise 5: Command Pattern
Implement a text‑based calculator using a registry of commands.
while True: cmd = input("Enter command (add/sub/mul/div) or 'quit': ") if cmd == "quit": break if cmd in commands: try: a = float(input("a: ")) b = float(input("b: ")) print(commands[cmd](a, b)) except ValueError: print("Invalid numbers") else: print("Unknown command")
</details>
---
### 🏠 Homework – Deeper Thinking
#### Short Answer Questions
**1. Function Composition with Partial Application**
Extend `compose` to allow partial application using `functools.partial`.
<details><summary>Sample Answer</summary>
```python
from functools import partial
def compose(*funcs):
def composed(x):
for f in reversed(funcs):
x = f(x)
return x
return composed
def add(a, b): return a + b
def square(x): return x * x
add5 = partial(add, 5)
h = compose(square, add5) # square(add5(x))
print(h(3)) # (3+5)^2 = 64
2. Build a Function Pipeline from a Configuration
Write build_pipeline(config) that applies a list of functions with parameters.
config = [ {'func': lambda x, power=2: x**power, 'params': {'power': 3}}, {'func': lambda x, factor=1: x *factor, 'params': {'factor': 2}} ] pipeline = build_pipeline(config) print(pipeline(2)) # (2^3)*2 = 16
</details>
**3. Function Metrics Collector**
Write a decorator `metrics` that adds `call_count`, `total_time`, and a `report()` method.
<details><summary>Sample Answer</summary>
```python
import time
from functools import wraps
def metrics(func):
func.call_count = 0
func.total_time = 0.0
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
func.call_count += 1
func.total_time += elapsed
return result
def report():
avg = func.total_time / func.call_count if func.call_count else 0
print(f"Call count: {func.call_count}")
print(f"Total time: {func.total_time*1000:.2f}ms")
print(f"Average time: {avg*1000:.2f}ms")
wrapper.report = report
return wrapper
@metrics
def slow():
time.sleep(0.1)
slow()
slow()
slow.report()
4. Function Registry with Plugin System
Design a plugin system that loads functions from external modules and registers them.
registry = {}
def register_command(name): def decorator(func): registry[name] = func return func return decorator
def load_plugins(module_name): module = importlib.import_module(module_name) for attr in dir(module): if attr.startswith('plugin_'): func = getattr(module, attr) if callable(func): registry[attr] = func
load_plugins('plugin_example') print(registry'plugin_hello')
</details>
**5. Function Equality and Identity – Deep Comparison**
Write a function `functions_equal(f, g)` that compares functions by attributes like name, defaults, and closure cells.
<details><summary>Sample Answer</summary>
```python
def functions_equal(f, g):
# Compare basic attributes
if f.__name__ != g.__name__:
return False
if f.__defaults__ != g.__defaults__:
return False
# Compare closure cells
if f.__closure__ is None and g.__closure__ is None:
return True
if f.__closure__ is None or g.__closure__ is None:
return False
if len(f.__closure__) != len(g.__closure__):
return False
for cell_f, cell_g in zip(f.__closure__, g.__closure__):
if cell_f.cell_contents != cell_g.cell_contents:
return False
return True
def make_adder(n):
return lambda x: x + n
add5 = make_adder(5)
add5_2 = make_adder(5)
add6 = make_adder(6)
print(functions_equal(add5, add5_2)) # True
print(functions_equal(add5, add6)) # False
functools.partial to handle partial application; or check the number of arguments passed and return a function if not enough.functools.wraps; define wrapper with attributes; store call count and total time.importlib.import_module; iterate over dir(module); check names and register.__name__, __module__, __defaults__, __closure__ (comparing cell contents). Source code comparison via inspect.getsource is fragile. Discuss trade‑offs.In this tutorial, you have learned:
__name__, __doc__, __defaults__, and __annotations__.inspect module.Understanding functions as first‑class objects is essential for leveraging Python’s full expressiveness. It allows you to write flexible, reusable, and elegant code. This knowledge also underpins many advanced topics like decorators, context managers, and functional programming techniques.
Next Steps: In the next tutorials, you will dive deeper into functional programming, exploring tools like itertools and functools, and also learn about object‑oriented programming to complement your functional toolkit.
Happy function‑playing!