Python and the Magic of First-Class Functions

Anik SikderAnik Sikder
4 min read

When people hear “first-class functions,” it can sound intimidating, like some high-level computer science buzzword. But in Python, the idea is surprisingly simple and incredibly powerful: functions are just like any other value.

You can pass them around, return them, tuck them into a dictionary, or hand them over to another function. Once you start using this feature, you’ll see Python in a new light.

Let’s take a journey through the key ideas, with examples that stick.

Functions as values

def greet(name):
    return f"Hello, {name}!"

# Store it in a variable
say_hi = greet

# Pass it as an argument
def make_loud(func, name):
    return func(name).upper()

print(say_hi("Sara"))              # Hello, Sara!
print(make_loud(greet, "Sara"))    # HELLO, SARA!

That’s the essence: a function can go anywhere a variable can.

Why docstrings and annotations matter

Python doesn’t force types on you, but leaving hints is a lifesaver for both humans and machines.

from typing import Callable, List

def transform(values: List[int], fn: Callable[[int], float]) -> List[float]:
    """Apply a function to each value and return a new list."""
    return [fn(v) for v in values]

Docstrings = purpose and usage.
Annotations = expectations and contracts.

They make code easier to read, maintain, and integrate with editors or tools.

Lambda: pocket-sized functions

Instead of writing a full function with def, you can spin up a one-liner:

square = lambda x: x * x
print(square(6))  # 36

They shine when combined with tools like sorted:

products = [
    {"name": "Keyboard", "price": 59},
    {"name": "Mouse", "price": 20},
    {"name": "Monitor", "price": 199},
]

sorted_products = sorted(products, key=lambda p: p["price"])

Quick, clean, and right to the point.

The quirky trick: shuffle with sorted

Ever seen someone randomize a list using… sorted?

import random
data = [1, 2, 3, 4, 5]
shuffled = sorted(data, key=lambda _: random.random())

It works, but is more of a fun hack than a best practice. For serious work, random.shuffle is better. Still, it’s a neat demonstration of how far key functions can go.

Introspection: looking inside functions

Python lets you peek into a function’s blueprint:

import inspect

def price_with_tax(price: float, rate: float = 0.1) -> float:
    """Calculate total price including tax."""
    return price * (1 + rate)

print(price_with_tax.__name__)          # price_with_tax
print(price_with_tax.__annotations__)   # {'price': float, 'rate': float, 'return': float}
print(inspect.signature(price_with_tax)) # (price: float, rate: float=0.1) -> float

This ability powers tools like Django, FastAPI, and CLIs that auto-generate help text.

Callables aren’t just functions

Any object with a __call__ method can behave like a function:

class Counter:
    def __init__(self):
        self.count = 0
    def __call__(self):
        self.count += 1
        return self.count

c = Counter()
print(c())  # 1
print(c())  # 2

This blends data with behavior, which can be incredibly elegant.

Map, filter, zip, and comprehensions

These are the assembly-line operators of Python:

names = ["anik", "sara", "lee"]

# Capitalize all names
proper = list(map(str.title, names))  
# ['Anik', 'Sara', 'Lee']

# Keep only passing scores
scores = [95, 45, 82]
passed = list(filter(lambda s: s >= 60, scores))  
# [95, 82]

# Pair students with scores
students = ["Anik", "Sara", "Lee"]
grades = [95, 88, 77]
paired = list(zip(students, grades))  
# [('Anik', 95), ('Sara', 88), ('Lee', 77)]

List comprehensions often make this even clearer:

proper = [n.title() for n in names]

Reduce: folding into one value

reduce takes a sequence and boils it down:

from functools import reduce
from operator import mul

nums = [2, 3, 4]
product = reduce(mul, nums, 1)   # 24

Neat trick, but when possible, built-ins like sum, any, max are cleaner.

Partial functions: pre-filled defaults

Use functools.partial to create functions with some arguments locked in:

from functools import partial

def add_tax(price, rate):
    return price * (1 + rate)

bd_tax = partial(add_tax, rate=0.15)
print(bd_tax(100))  # 115.0

This shines in config-heavy code (APIs, formatting, logging).

The operator module: functional shortcuts

Why write tiny lambdas when Python already has helpers?

from operator import itemgetter

cities = [
    {"name": "Dhaka", "pop": 21_000_000},
    {"name": "Chattogram", "pop": 2_600_000},
]
largest = max(cities, key=itemgetter("pop"))

Readable, fast, and built for exactly these cases.

Wrapping it all up

First-class functions aren’t just an academic concept. They’re the engine of flexibility in Python:

  • Compose small behaviors into big workflows.

  • Keep your code DRY, elegant, and maintainable.

  • Use the standard library (functools, operator, itertools) like superpowers.

The beauty of Python here is that it doesn’t force you, but once you start writing functions as data, your code unlocks a whole new level.

0
Subscribe to my newsletter

Read articles from Anik Sikder directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anik Sikder
Anik Sikder

Full-Stack Developer & Tech Writer specializing in Python (Django, FastAPI, Flask) and JavaScript (React, Next.js, Node.js). I build fast, scalable web apps and share practical insights on backend architecture, frontend performance, APIs, and Web3 integration. Available for freelance and remote roles.