Functions as First-Class Citizens

Intermediate · 20 min read · ▶ live playground · ✦ checkpoint

You already know how to call a function. The next step is stranger: in Python, a function is also a value you can store, pass around, and return from another function.

Functions are objects too

A first-class object is a value the language lets you treat like any other value: bind it to a name, put it in a collection, pass it as an argument, or return it. Python functions are first-class objects.

Think back to the recipe-card analogy from 7.1. Calling a function is using the recipe. Treating the function as an object is handling the card itself: you can put it in a binder, hand it to someone else, or choose which card to use later.

def cheer(player):
    return f"Go, {player}!"
 
def whisper(player):
    return f"You've got this, {player}."
 
message_maker = cheer
messages = [cheer, whisper]
 
print(message_maker("Mina"))
 
for make_message in messages:
    print(make_message("Ravi"))

Go, Mina!
Go, Ravi!
You've got this, Ravi.

Notice the missing parentheses in message_maker = cheer. That line does not call cheer; it stores the function object under another name. Parentheses mean "run it now." No parentheses means "refer to the function itself."

Pass behavior as an argument

A higher-order function is a function that takes another function as an argument, returns a function, or both. This lets one function control the shape of the work while another function supplies the behavior.

def loud_message(player):
    return f"{player.upper()} is ready"
 
def calm_message(player):
    return f"{player} is steady"
 
def announce_players(players, make_message):
    for player in players:
        print(make_message(player))
 
team = ["Mina", "Ravi", "Noor"]
 
announce_players(team, loud_message)
announce_players(team, calm_message)

MINA is ready
RAVI is ready
NOOR is ready
Mina is steady
Ravi is steady
Noor is steady

announce_players() does not care how a message is made. It only knows that make_message is something it can call with a player name. That is the useful part: the loop lives in one place, while the behavior can change.

This pattern appears everywhere in Python. You give a function a small bit of behavior, and it applies that behavior to data.

Return a function

Functions can also create and return other functions. The outer function builds the recipe card. The returned function is the card you use later.

def make_bonus_adder(bonus):
    def add_bonus(score):
        return score + bonus
 
    return add_bonus
 
small_bonus = make_bonus_adder(5)
big_bonus = make_bonus_adder(20)
 
print(small_bonus(40))
print(big_bonus(40))

45
60

make_bonus_adder(5) returns a function. That returned function remembers the bonus value from the enclosing scope you studied in 7.3. You do not need to use this pattern constantly yet. You do need to recognize it: a function call can hand you back a function object, not just a number or string.

Use lambda for tiny functions

A lambda function is a small anonymous function, which means a function with no def name of its own, written as an expression with lambda.

players = ["Mina", "Ravi", "Noor"]
scores = [42, 35, 48]
 
shout = lambda player: player.upper()
 
print(shout("Mina"))
print(list(map(lambda score: score + 10, scores)))
print(list(map(lambda player: f"Player: {player}", players)))

MINA
[52, 45, 58]
['Player: Mina', 'Player: Ravi', 'Player: Noor']

The shape is:

  • lambda player: player.upper() means "take player, return player.upper()."
  • The part before : names the parameter.
  • The part after : is the returned expression.

Use lambda when the function is tiny and used right where it is written. If the logic needs a real name, multiple steps, or a docstring, use def. Clear beats short.

First look at map, filter, and sorted

map() is a built-in function that applies one function to each item in an iterable. filter() is a built-in function that keeps only items where a function returns a truthy value. Their results are lazy, which means Python waits to produce each value until something asks for it, so wrap them in list() when you want to see all the values now.

scores = [42, 35, 48, 29]
players = [
    {"name": "Mina", "score": 42},
    {"name": "Ravi", "score": 35},
    {"name": "Noor", "score": 48},
]
 
def add_curve(score):
    return score + 5
 
def is_passing(score):
    return score >= 40
 
print(list(map(add_curve, scores)))
print(list(filter(is_passing, scores)))
print(sorted(players, key=lambda player: player["score"]))

[47, 40, 53, 34]
[42, 48]
[{'name': 'Ravi', 'score': 35}, {'name': 'Mina', 'score': 42}, {'name': 'Noor', 'score': 48}]

sorted() already returns a list. Its key argument accepts a function that tells Python what to sort by. In key=lambda player: player["score"], Python calls that tiny function for each dictionary and sorts by the returned score.

You can often write the same idea with a loop or a list comprehension. That is fine. These tools are worth knowing because Python code often says "apply this function to each item" or "sort by this calculated value."

Try it - choose the behavior

Run this, then change mode to "quiet" or "score". You are not changing the loop that prints the roster; you are changing which function the loop uses.

python — playgroundlive
⌘/Ctrl + Enter to run

This is the whole first-class function idea in one small program. choose_labeler() returns a function. make_label stores that function. sorted() receives a tiny lambda for its key, and map() plus filter() receive tiny functions for transforming and keeping players.

Checkpoint

Answer all three to mark this lesson complete

Functions are now more than reusable blocks; they are values your program can choose, store, and pass around. Next, type hints give functions small labels for expected values and returned values without changing how Python runs them.

+50 XP on completion