Built-in Functions

Python ships with dozens of functions always available without any import -- len, sorted, zip, isinstance, and friends -- that cover the everyday operations most other languages make you write by hand.

Every Python program has access to a fixed set of built-in functions without writing a single import statement. They live in the builtins module, which Python implicitly makes available in every namespace. This topic tours the most commonly used ones: sizing and aggregation, sorting, pairing iterables, functional transformation, numeric helpers, and runtime introspection.

What Are Built-in Functions

Built-in functions are objects available in every Python namespace by default, implemented in the builtins module (CPython implementation detail: mostly written in C for speed). You can call len(x) or sorted(x) anywhere without importing anything, because Python's name-resolution rules check the builtins scope as a final fallback after local, enclosing, and global scopes (the LEGB rule).

Built-ins are ordinary objects; you can inspect the builtins module directly, though you never need to import it to use its contents.

Shadowing a built-in name (e.g. writing list = [1, 2, 3]) rebinds that name in your current scope; it does not delete the built-in, but it does hide it for the rest of that scope. Avoid reusing built-in names as variable names.

import builtins
print(len(dir(builtins)))  # number of names in the builtins module
print(callable(builtins.len))  # True -- len is itself an object

Sizing and Aggregation: len, sum, min, max

len(obj) returns the number of items in a container by calling its __len__ method. sum(iterable, start=0) adds all items, starting from an optional initial value. min and max scan an iterable (or take multiple positional arguments) and return the smallest/largest item, optionally guided by a key function.

key= is accepted by min, max, and sorted alike: it's a function applied to each element before comparison, not a replacement for the returned value.

scores = [88, 92, 79, 95, 84]
print(len(scores))            # 5
print(sum(scores))            # 438
print(sum(scores, 100))       # 538 -- custom start value
print(min(scores), max(scores))  # 79 95

words = ['fig', 'apple', 'kiwi']
print(max(words, key=len))    # 'apple' -- longest word

Sorting and Reversing: sorted, reversed

sorted(iterable, key=None, reverse=False) returns a new list in ascending order without mutating the input (unlike list.sort(), which sorts in place and returns None). reversed(seq) returns a lazy iterator that yields items back to front; it requires the argument to support __reversed__ or be a sequence with __len__ and __getitem__.

sorted() always builds a new list, so it's safe to use on tuples, strings, and any iterable, not just lists.

nums = [4, 1, 3, 2]
print(sorted(nums))            # [1, 2, 3, 4] -- new list
print(nums)                    # [4, 1, 3, 2] -- unchanged
print(sorted(nums, reverse=True))  # [4, 3, 2, 1]

people = [('Ana', 30), ('Bo', 25)]
print(sorted(people, key=lambda p: p[1]))  # sorted by age

print(list(reversed(nums)))    # [2, 3, 1, 4]

Pairing Iterables: enumerate and zip

enumerate(iterable, start=0) yields (index, item) pairs lazily, avoiding manual counter variables. zip(*iterables) yields tuples pairing up items at the same position across multiple iterables, stopping as soon as the shortest input is exhausted.

Both enumerate and zip return iterators, not lists -- wrap them in list() to materialize, or consume them directly in a for loop.

itertools.zip_longest (standard library, needs an import) fills missing values instead of truncating, when you need every element from the longer iterable.

fruits = ['apple', 'kiwi', 'plum']
for i, fruit in enumerate(fruits, start=1):
    print(i, fruit)
# 1 apple
# 2 kiwi
# 3 plum

names = ['Ada', 'Grace']
years = [1815, 1906, 1912]
print(list(zip(names, years)))  # [('Ada', 1815), ('Grace', 1906)] -- stops at shorter

Functional Transformation: map and filter

map(func, iterable, ...) applies func to each item and returns a lazy iterator of results; with multiple iterables, func is called with one argument from each, stopping at the shortest. filter(func, iterable) keeps only items for which func(item) is truthy; filter(None, iterable) keeps only truthy items directly.

In modern Python, a list/generator comprehension (e.g. [n ** 2 for n in nums]) is usually preferred over map/filter for readability -- both approaches are idiomatic, but comprehensions are covered in their own topic.

nums = [1, 2, 3, 4, 5]
squares = list(map(lambda n: n ** 2, nums))
print(squares)                 # [1, 4, 9, 16, 25]

evens = list(filter(lambda n: n % 2 == 0, nums))
print(evens)                   # [2, 4]

Boolean Aggregation: any and all

any(iterable) returns True if at least one item is truthy (False for an empty iterable); all(iterable) returns True only if every item is truthy (True for an empty iterable, vacuously). Both short-circuit: they stop consuming the iterable as soon as the answer is determined.

Passing a generator expression (no brackets) avoids building an intermediate list, and lets any/all stop early without evaluating the rest.

ages = [17, 22, 19, 30]
print(any(a >= 18 for a in ages))  # True -- at least one adult
print(all(a >= 18 for a in ages))  # False -- not everyone is an adult
print(all([]))                     # True -- vacuous truth
print(any([]))                     # False

Numeric Built-ins: abs, round, pow, divmod

abs(x) returns the absolute value. round(x, ndigits=None) rounds to the nearest value with ndigits decimal places (or to the nearest int when ndigits is omitted), using round-half-to-even (banker's rounding) to reduce systematic bias. pow(base, exp, mod=None) computes base ** exp, or efficient modular exponentiation when mod is given. divmod(a, b) returns (a // b, a % b) as a single tuple.

round(2.5) giving 2 (not 3) surprises many newcomers -- it's round-half-to-even, and floating-point representation can also make the 'nearest' value not exactly what decimal intuition expects.

print(abs(-7), abs(3.2))       # 7 3.2
print(round(2.5), round(3.5))  # 2 4 -- round-half-to-even, not always 'up'
print(round(3.14159, 2))       # 3.14
print(pow(2, 10))               # 1024
print(pow(7, 128, 13))          # fast modular exponentiation
print(divmod(17, 5))            # (3, 2)

Type Introspection: isinstance and issubclass

isinstance(obj, cls_or_tuple) checks whether obj is an instance of a class (or any class in a tuple of classes), respecting inheritance. issubclass(cls, cls_or_tuple) checks the same relationship between two classes directly. Both are the idiomatic way to branch on type in Python, preferring them over comparing type(obj) == cls, which breaks for subclasses.

class Animal: pass
class Dog(Animal): pass

d = Dog()
print(isinstance(d, Dog))          # True
print(isinstance(d, Animal))       # True -- Dog is a subclass of Animal
print(isinstance(d, (int, str)))   # False -- tuple means 'any of these'
print(issubclass(Dog, Animal))     # True
print(type(d) == Animal)           # False -- exact-type check misses inheritance

Dynamic Attribute Access: getattr, setattr, hasattr

getattr(obj, name, default) fetches an attribute by string name, returning default (or raising AttributeError if no default is given) when it's missing. setattr(obj, name, value) assigns an attribute by string name -- equivalent to obj.name = value but usable when the name is only known at runtime. hasattr(obj, name) reports whether accessing that attribute would succeed (it's implemented as getattr wrapped in a try/except).

These three are how you work with attribute names that only exist as strings at runtime -- config-driven code, serialization, and dynamic dispatch all lean on this trio.

class Config:
    debug = False

cfg = Config()
field = 'timeout'
setattr(cfg, field, 30)
print(getattr(cfg, field))            # 30
print(getattr(cfg, 'missing', 'n/a'))  # 'n/a' -- no AttributeError
print(hasattr(cfg, 'debug'))          # True

callable(), dir(), and help()

callable(obj) reports whether obj can be called with () -- true for functions, classes, methods, and any object whose class defines __call__. dir(obj) lists the names available on an object (attributes and methods), useful for interactive exploration. help(obj) prints an object's docstring and signature information in an interactive session.

dir() and help() are primarily interactive/exploratory tools -- reach for them at a REPL or notebook when you're not sure what an object offers, rather than in production code.

print(callable(len))       # True
print(callable(42))        # False
print(callable(str))       # True -- classes are callable (they construct instances)

print('upper' in dir(str))  # True -- str objects have an upper() method
# help(str.upper)  # in an interactive session, prints str.upper's docstring

Combining Built-ins in Practice

Built-ins compose naturally: it's idiomatic to chain sorted with a key, enumerate over a zip, or filter before mapping. Reading these compositions right-to-left (innermost call first) usually makes the data flow clear.

students = [('Ana', 88), ('Bo', 95), ('Cy', 72)]
top = sorted(students, key=lambda s: s[1], reverse=True)[:2]
for rank, (name, score) in enumerate(top, start=1):
    print(f"{rank}. {name}: {score}")
# 1. Bo: 95
# 2. Ana: 88

Quick Reference

len(x)                 count of items
sum(x, start=0)         total, with optional start
min(x) / max(x)         smallest / largest, key= supported
sorted(x, key=, reverse=)  new sorted list
reversed(x)             lazy reverse iterator
enumerate(x, start=0)    lazy (index, item) pairs
zip(*xs)                lazy tuples of aligned items, stops at shortest
map(f, x) / filter(f, x)  lazy transform / lazy keep-if-truthy
any(x) / all(x)          short-circuit boolean aggregation
abs / round / pow / divmod  numeric helpers
isinstance / issubclass  type-relationship checks (inheritance-aware)
getattr/setattr/hasattr  attribute access by string name
callable(x)              can x be called with ()?
dir(x) / help(x)         interactive introspection

Key Takeaways

Built-in functions live in the builtins module and are always available without import, via Python's LEGB name-resolution rule.

sorted() and reversed() return new objects and never mutate their input; list.sort() and list.reverse() do mutate in place.

map, filter, zip, enumerate, and reversed all return lazy iterators -- wrap in list() to see all items at once or consume with a for loop.

isinstance/issubclass respect inheritance; comparing type(obj) == cls does not.

getattr/setattr/hasattr let you work with attribute names that are only known as strings at runtime.

round() uses round-half-to-even, not always-round-up, which can surprise decimal intuition.

if type(x) == list: ...

if isinstance(x, list): ...

isinstance() respects subclassing; comparing type(x) == list rejects any subclass of list even though it genuinely is a list.

nums.sort() new_nums = nums # thinking this is a separate sorted copy new_nums = sorted(nums) # nums is left unchanged list.sort() mutates in place and returns None; sorted() returns a new list and leaves the original untouched. Confusing the two is a very common bug.

squares = map(lambda n: n 2, range(5)) print(len(squares)) # TypeError squares = list(map(lambda n: n 2, range(5))) print(len(squares)) map() returns a lazy iterator, not a list -- it has no len() and can only be iterated once. Materialize it with list() first if you need indexing, length, or repeated iteration.

if round(2.5) == 3: ... # assumes round() always rounds .5 up

Python's round() uses banker's rounding (round-half-to-even) to avoid systematic bias, so round(2.5) is 2 and round(3.5) is 4 -- not the 'always round .5 up' rule taught in school.

What is the difference between sorted(x) and x.sort() when x is a list?

sorted(x) returns a new sorted list and leaves x unchanged; x.sort() sorts the list in place and returns None. sorted() also works on any iterable, not just lists.

Why does zip([1, 2, 3], ['a', 'b']) only produce two pairs?

zip() stops as soon as its shortest input iterable is exhausted, so pairing a 3-item list with a 2-item list yields only 2 tuples; the third item of the longer list is silently dropped.

Why is isinstance(obj, Base) usually better than type(obj) == Base for type checks?

isinstance() accounts for inheritance -- an instance of a subclass of Base still passes isinstance(obj, Base) but fails type(obj) == Base, which checks for the exact class only.

What does getattr(obj, 'name', None) do differently from obj.name?

obj.name raises AttributeError if the attribute is missing; getattr(obj, 'name', None) returns None (or any given default) instead, letting you avoid a try/except when a missing attribute is an expected possibility.

Why does all([]) return True while any([]) returns False?

all() asks whether every item is truthy, which is vacuously true when there are no items to violate it; any() asks whether at least one item is truthy, which cannot be satisfied by zero items.

What kind of object does map(func, iterable) return, and what must you do to see all its results as a list?

It returns a lazy map iterator that computes each result on demand as it's consumed; wrap it in list(...) (or iterate it in a for loop) to materialize or see all the transformed values.

How do min() and max() use a key function, and how is that different from what they return?

key= supplies a function applied to each element to determine the comparison value, but min/max still return the original element itself, not the key's output -- e.g. max(words, key=len) returns the longest word, not its length.

Write a function that takes a list of (name, score) tuples and returns the names of the top 3 scorers, highest first, using only built-in functions.

def top_scorers(records: list[tuple[str, int]], n: int = 3) -> list[str]: ranked = sorted(records, key=lambda r: r[1], reverse=True) return [name for name, _ in ranked[:n]] print(top_scorers([('Ana', 88), ('Bo', 95), ('Cy', 72), ('Di', 91)])) sorted(records, key=lambda r: r[1], reverse=True) orders the tuples by score descending without mutating the input list; slicing [:n] takes the first n, and the comprehension extracts just the names.

What does this print, and why does it stop where it does?

[(0, 'a'), (1, 'b'), (2, 'c')] -- zip() pairs items positionally and stops as soon as the shorter iterable ('abc', length 3) is exhausted, ignoring the remaining values 3 and 4 from range(5).

Write a function using any() and a generator expression to check whether a list of numbers contains at least one value greater than 100, without building an intermediate list.

def has_large_value(nums: list[int]) -> bool: return any(n > 100 for n in nums) print(has_large_value([10, 55, 250, 3])) # True print(has_large_value([10, 55, 99])) # False any() consumes the generator expression lazily and short-circuits as soon as it finds a truthy value (n > 100), so for [10, 55, 250, 3] it stops at 250 without checking the remaining item.

What does this print?

False then blue -- hasattr returns False because Widget instances start with no color attribute; setattr adds one dynamically, so the following getattr finds it and returns 'blue' instead of falling back to the 'unknown' default.

print(list(zip(range(5), 'abc')))
class Widget:
    pass

w = Widget()
print(hasattr(w, 'color'))
setattr(w, 'color', 'blue')
print(getattr(w, 'color', 'unknown'))