If you’ve ever handed someone a tool instead of doing the job yourself, you already get first-class functions. In Python, functions are values: you can store them in variables, pass them to other functions, return them, put them in lists and dicts, the whole VIP treatment. Once this clicks, a lot of Python suddenly feels smoother and more powerful.
Below is a practical, human-sized tour. We’ll keep the words simple, the examples real, and the pace lively.
What “first-class” really means
def greet(name):
return f"Hello, {name}!"
say_hello = greet # assign
toolbox = {"hi": greet} # store in dict
def loud(func, name): # pass as argument
return func(name).upper()
def greeter(prefix): # return a function
def inner(name):
return f"{prefix} {name}"
return inner
print(say_hello("Anik")) # Hello, Anik!
print(loud(greet, "Anik")) # HELLO, ANIK!
print(greeter("Welcome")("Anik")) # Welcome Anik
That’s it. Everything else in this article builds on this idea.
Docstrings & annotations: tiny notes, huge payoffs
Docstrings explain why and how. Type annotations hint what goes in and out. They don’t change runtime behavior, but they help your future self (and your team, editors, linters, and tests).
from typing import Iterable, Callable, TypeVar, List
T = TypeVar("T")
U = TypeVar("U")
def transform(items: Iterable[T], fn: Callable[[T], U]) -> List[U]:
"""
Apply a function to each item and return a list.
Args:
items: Any iterable of values (list, tuple, generator…)
fn: A function that takes a T and returns a U.
Returns:
A list of transformed values.
"""
return [fn(x) for x in items]
A good rule: if a function has non-obvious behavior, add a docstring. If a parameter type isn’t obvious, annotate it.
Quick real-life example:
You’re processing user input from a CSV. Annotate your converter functions so teammates know what shape to expect.
Lambda expressions: tiny functions on the fly
A lambda is a small, one-expression function you define inline.
square = lambda x: x * x
print(square(5)) # 25
Use lambdas for short glue code, especially with sorting and functional helpers.
When to avoid: if the logic needs a name, multiple steps, or a docstring use def
.
Real-life example: remove currency symbols and convert to float
.
prices = ["$12.99", "$5.50", "$100.00"]
to_number = lambda s: float(s.replace("$", ""))
clean = [to_number(p) for p in prices] # [12.99, 5.5, 100.0]
Lambdas and sorting: turning chaos into order
Python’s sorted
loves a key
function. Lambdas shine here.
products = [
{"name": "Keyboard", "price": 59.99},
{"name": "Mouse", "price": 25.00},
{"name": "Monitor", "price": 199.00},
]
# Sort by price ascending
by_price = sorted(products, key=lambda p: p["price"])
# Sort by name length, then alphabetically
by_len_then_name = sorted(
products,
key=lambda p: (len(p["name"]), p["name"])
)
Tip: For speed and readability when sorting dicts/objects, operator.itemgetter
and operator.attrgetter
(later section) are great.
A playful challenge: “randomize” an iterable using sorted
(!!)
“Wait, sorted to shuffle?” Yes, as a trick:
import random
data = [1, 2, 3, 4, 5]
randomized = sorted(data, key=lambda _: random.random())
Each item gets a random key; sorting by those keys yields a random order.
Honest note: this is a cute hack. Prefer random.shuffle(data)
for in-place shuffling, it’s faster and clearer. But sometimes you need a non-destructive shuffle (keep the original list unchanged); the sorted(..., key=...)
trick works nicely.
Function introspection: peeking under the hood
Python lets you ask functions about themselves.
import inspect
def price_with_tax(price: float, rate: float = 0.15) -> float:
"""Return price plus tax."""
return round(price * (1 + rate), 2)
print(price_with_tax.__name__) # 'price_with_tax'
print(price_with_tax.__doc__) # docstring
print(price_with_tax.__annotations__) # {'price': float, 'rate': float, 'return': float}
print(price_with_tax.__defaults__) # (0.15,)
sig = inspect.signature(price_with_tax)
print(sig) # (price: float, rate: float=0.15) -> float
for name, param in sig.parameters.items():
print(name, param.default, param.annotation)
Real-life use: Build a small CLI or web router that reads annotations and default values to generate help text or form fields automatically.
Callables: not just functions
Anything that can be “called” with ()
is a callable. Functions, methods, lambdas, and objects with a __call__
method.
class RateLimiter:
def __init__(self, limit_per_minute: int):
self.limit = limit_per_minute
self.calls = 0
def __call__(self) -> bool:
# toy example: allow first N calls then block
self.calls += 1
return self.calls <= self.limit
allow = RateLimiter(3)
print(allow()) # True
print(allow()) # True
print(allow()) # True
print(allow()) # False
You can check callability:
callable(len) # True
callable(RateLimiter) # True (classes are callable—they construct objects)
callable(allow) # True
Why it’s cool: You can pass around behavior with state. Great for caching, throttling, validators, and formatters.
Map, filter, zip & list comprehensions
These are Python’s little assembly line tools.
map(fn, iterable)
Apply a function to each item.
names = ["anik", "sara", "LEE"]
proper = list(map(str.title, names)) # ['Anik', 'Sara', 'Lee']
filter(fn, iterable)
Keep items where fn(item)
is truthy.
scores = [95, 50, 77, 88]
passed = list(filter(lambda s: s >= 60, scores)) # [95, 77, 88]
zip(a, b, ...)
Pair items by position.
students = ["Anik", "Sara", "Lee"]
grades = [95, 88, 77]
paired = list(zip(students, grades)) # [('Anik', 95), ('Sara', 88), ('Lee', 77)]
Unzip with the star operator:
names2, marks2 = zip(*paired)
List comprehensions
Often clearer (and Pythonic) than map
/filter
:
proper = [n.title() for n in names]
passed = [s for s in scores if s >= 60]
Real-life example: Clean CSV rows.
rows = [
{"name": " anik ", "age": "23"},
{"name": "SARA", "age": " 19 "},
]
clean_rows = [
{"name": r["name"].strip().title(), "age": int(r["age"])}
for r in rows
]
Reducing functions: folding a sequence into one value
functools.reduce
applies a function cumulatively. You can write many things with reduce
, but in Python we often prefer built-ins (sum
, min
, max
, any
, all
) because they’re clearer and faster.
from functools import reduce
from operator import mul
nums = [2, 3, 4]
product = reduce(mul, nums, 1) # 24
Running totals (folding):
from functools import reduce
transactions = [100, -20, -5, 50]
balance = reduce(lambda acc, t: acc + t, transactions, 0) # 125
Better alternatives when they exist:
sum(transactions) # 125
any(x < 0 for x in transactions) # True
max(prices, default=0.0) # simple and clear
Real-life example: count word frequency (but see note!).
from functools import reduce
words = "to be or not to be".split()
freq = reduce(
lambda acc, w: (acc.update({w: acc.get(w, 0) + 1}) or acc),
words,
{}
)
# {'to': 2, 'be': 2, 'or': 1, 'not': 1}
Note: That trick is educational, but in real projects use collections.Counter
clearer and quicker.
Partial functions: pre-filling arguments for later
functools.partial
creates a new function with some arguments fixed. It’s like setting a default once, then reusing it everywhere.
from functools import partial
def apply_tax(price: float, rate: float) -> float:
return round(price * (1 + rate), 2)
vat_bd = partial(apply_tax, rate=0.15) # Bangladesh example VAT ~15%
print(vat_bd(100)) # 115.0
Real-life examples:
- Formatting and parsing:
int_base2 = partial(int, base=2)
print(int_base2("1011")) # 11
- Custom rounding:
round2 = partial(round, ndigits=2)
print(round2(3.14159)) # 3.14
-
Preconfigured validators, loggers, or HTTP calls (e.g.,
get_json = partial(requests.get, timeout=5)
), keeping your code DRY.
The operator
module: tiny, fast, readable helpers
operator
provides function versions of Python’s operators and common access patterns. They’re fast and pair beautifully with sorted
, map
, and reduce
.
from operator import itemgetter, attrgetter, methodcaller, add, mul
itemgetter
for dicts/tuples
rows = [
{"city": "Dhaka", "pop": 21_000_000},
{"city": "Chattogram", "pop": 2_600_000},
]
top = max(rows, key=itemgetter("pop"))
# {'city': 'Dhaka', 'pop': 21000000}
attrgetter
for objects
class User:
def __init__(self, name, age): self.name, self.age = name, age
users = [User("Anik", 23), User("Sara", 21)]
youngest = min(users, key=attrgetter("age"))
methodcaller
call a named method
caps = list(map(methodcaller("upper"), ["hi", "there"])) # ['HI', 'THERE']
Arithmetic operators
from functools import reduce
total = reduce(add, [1, 2, 3, 4], 0) # 10
area_scale = reduce(mul, [2, 3, 4], 1) # 24
Why use these? They read like English, avoid tiny lambdas, and can be a tad faster.
Patterns you’ll reuse a lot
- Strategy pattern (choose behavior at runtime):
strategies = {
"percent": lambda price, v: price * (1 - v),
"flat": lambda price, v: price - v,
}
def apply_discount(kind, price, value):
return strategies[kind](price, value)
- Pipelines:
def pipe(x, *funcs):
for f in funcs: x = f(x)
return x
result = pipe(
" hello ",
str.strip,
str.title,
lambda s: f"{s}!"
)
# 'Hello!'
- Callbacks for events / hooks: pass functions to be called later (GUI events, web hooks, retries).
Tiny “try it” moments (pick a couple!)
-
Sort by multiple keys: sort a list of student dicts by score desc, then name asc. Use
itemgetter
and a negative score orkey=lambda s: (-s['score'], s['name'])
. -
Callable object: make a
Slugify
class that remembers allowed characters and converts titles to URL slugs via__call__
. -
Partial in the wild: create
round_bd = partial(round, ndigits=0)
and use it inside amap
to tidy numbers from a sensor.
Wrap-up
First-class functions are not a fancy trick; they’re the heart of “Pythonic” code:
- They let you compose behavior like Lego bricks.
- They keep your code dry, testable, and flexible.
- They play nicely with the standard library:
functools
,operator
,itertools
, and friends.
What’s a small, elegant function you reach for again and again maybe a tiny formatter, a filter you’re proud of, or a callable class that cleaned up a messy workflow? The best patterns often start small and travel far.
Top comments (0)