skip to content

Function Objects and Attributes

A def binds an ordinary object you can store in a dict, pass around or hang attributes on, carrying __name__, __qualname__ and __doc__. Interviewers ask when a dispatch table beats a chain of elifs.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

Why are Python functions called first-class objects, and what does `def` create?

level: juniorimportance: must knowfreq 68%

answer

  1. def runs; it does not declare
  2. the name is only a binding
  3. an object with attributes and a dict
  4. types.FunctionType, same as a lambda
  5. store it, pass it, return it

basics

~20 s

A def statement runs at runtime, builds a function object and binds it to a name, exactly like an assignment. That object can be stored in a dict, passed as an argument or returned again, which is what first-class means.

solid answer

~40 s

`def` is an executable statement, not a declaration. When it runs, the interpreter builds a function object and binds it to the `def`'s name in whatever namespace is executing — module globals, a class body, or a local scope. The result is an ordinary object of type `types.FunctionType`, carrying `__name__`, `__qualname__`, `__doc__`, `__module__` and a writable `__dict__`. Because it is just a value, it can be aliased, stored in a list or dict, passed to another function and returned from one — that is all "first-class" means. The bare name is the object; the parentheses are the call, which is why `register(rebuild())` registers a return value rather than the function. `callable(obj)` reports whether an object's type supports call syntax, so classes and C builtins qualify too.

code

python · 12 lines
python
import types

def shout(text):
    return text.upper() + "!"

def whisper(text):
    return text.lower() + "..."

styles = {"shout": shout, "whisper": whisper}
chosen = styles["shout"]
print(chosen("rebuild finished"))
print(callable(chosen), isinstance(shout, types.FunctionType))

go deeper

for a junior

Be ready to say what a def produces and to show a function stored in a dict or handed to another function. Recall that the bare name is the object and that adding parentheses calls it immediately.

for a middle

Explain that def executes and binds like an assignment, name the type behind a function object and the attributes it carries, and describe exactly what callable() interrogates.

for a senior

Show where first-class functions earn their keep in real code — callbacks, plugin registries, dispatch tables — and be honest about the traceability you lose when behaviour arrives as a value.

for a principal

Own the tradeoff: passing functions around makes behaviour configurable at runtime but moves control flow out of the reader's line of sight. Set the team convention for when that indirection is worth its cost.

### `def` is a statement, and statements execute Python has no separate declaration phase for functions. A `def` is an ordinary statement that runs when control reaches it. When it runs, the interpreter builds a **function object** from the body the compiler already prepared, fills in the defaults and the enclosing-scope information, and then **binds that object to the `def`'s name** in whichever namespace is currently executing — module globals at import time, a class namespace inside a class body, a local scope inside another function. The binding is an ordinary name binding: nothing about it is more magical than `x = 5`. Two consequences follow immediately. First, a `def` inside an `if` that never runs never creates anything; second, `del f` or `f = None` removes the *name*, not some registered declaration. ### What the object actually is The object is an instance of `types.FunctionType` — the same type a `lambda` produces, which is why `types.LambdaType is types.FunctionType`. It carries attributes you can read at runtime: - `__name__` — the bare name it was defined with. - `__qualname__` — the dotted path to it inside its module. - `__doc__` — the docstring, or `None`. - `__module__` — the name of the defining module. - `__defaults__` — the default values, evaluated once when the `def` ran. - `__dict__` — a writable per-object attribute dict. Functions written in C are a *different* type: `len` is a `types.BuiltinFunctionType`, and `inspect.isfunction(len)` is `False` while `inspect.isbuiltin(len)` is `True`. That distinction matters the moment you introspect: a C builtin has no writable `__dict__` and may not expose a signature to `inspect.signature`. ### "First-class" is a claim about what you may do with the value An object is first-class in a language when it can be treated like any other value. For Python functions that means all of the following are unremarkable: - **Bind it to another name.** `alias = rebuild` — one object, two names. - **Put it in a container.** A list of validators, a dict mapping a mode string to a handler, a tuple of hooks. - **Pass it as an argument.** Every `key=` argument, every callback, every `filter` predicate is this. - **Return it from a function.** A function that builds and returns another function is the whole basis of decorators and factory helpers. - **Attach data to it.** Because `__dict__` is writable, `handler.priority = 3` sticks. Notice that no special syntax is required to *refer* to a function without calling it. **The bare name is the object; the parentheses are the call.** This is the single most common beginner bug in this area: writing `register(rebuild())` registers whatever `rebuild` returned — very often `None` — instead of registering the function. The symptom appears far from the cause, when something later tries to call the stored `None`. ### `callable` and the wider family of callables `callable(obj)` reports whether `obj`'s **type** supports call syntax. It asks the type, not the instance, and it is a capability check rather than a promise: `callable(f)` being `True` says nothing about whether `f()` will raise for wrong arity. Function objects are callable; so are C builtins; so are classes, because calling a class constructs an instance; and so are instances of a class that defines the call protocol. `callable(3)` is `False`. The practical reading is that "a function" in a Python API contract almost always means "any callable". Code that insists on `types.FunctionType` via `isinstance` rejects perfectly good callables — a class, a partial application built with `functools.partial`, a callable instance — and is usually a design mistake. Duck-type on `callable` if you must check at all. ### Why this shows up in interviews Everything a Python codebase does with behaviour-as-data rests on this one fact. Sorting with a `key=` argument, wiring a callback into an event source, building a plugin registry, mapping a command name to the routine that implements it, memoising, retrying, timing — each of them passes, stores or returns a function object. A candidate who thinks of `def` as a declaration cannot explain any of them, and will reach for strings plus `eval`, or for an ever-growing conditional, where a Python programmer reaches for a dict of functions. ### Introspecting one in practice `inspect` is the readable front door: `inspect.isfunction(obj)` distinguishes a Python-level function, `inspect.signature(obj)` reports the parameters, and reading `__module__` together with `__qualname__` gives you the path you would put in a log line. All of it is plain attribute access on an ordinary object — which is the point.

  • What does the builtin `callable(obj)` actually tell you?
    It reports whether `obj`'s type supports call syntax — it interrogates the type, not the instance. Function objects, C builtins, classes (calling a class constructs an instance) and instances of a class defining the call protocol all return True; an `int` or a `list` returns False. It is a capability check, not a promise: `callable(f)` says nothing about whether `f()` will raise for the wrong arity.
  • How does `types.FunctionType` differ from `types.BuiltinFunctionType`?
    `types.FunctionType` is the type of a function written in Python; `type(f)` is that for both a `def` and a `lambda`, since `types.LambdaType` is the same object. Functions implemented in C, such as `len`, are `types.BuiltinFunctionType` — `inspect.isfunction(len)` is False and `inspect.isbuiltin(len)` is True. The practical difference is introspection: a C builtin has no writable `__dict__` and may not expose a signature.
  • How do you refer to a function without calling it, and what is the usual mistake?
    The bare name is already the object: `register(rebuild)` passes the function, `register(rebuild())` calls it first and passes whatever it returned, very often `None`. The failure surfaces far from the cause, when something later tries to call the stored value and raises `TypeError: 'NoneType' object is not callable`. Whenever a callback, a `key=` argument or a registry entry misbehaves, check for stray parentheses first.

A function name is a label on a jar, not the jar itself: you can hand someone the jar, put it on a shelf beside others, or write a new label for the same jar — opening it is a separate act.

saying these in an interview costs you the question

  • Claims def is a declaration processed before the module body runs
  • Thinks a function name is special syntax rather than an ordinary binding
  • Says only lambdas can be passed around as values
  • Passes handler() instead of handler and stores the return value
  • Believes callable() guarantees the call will succeed
  • Assumes a C builtin like len behaves identically to a def'd function

context

open as a page

Why is `callable(x)` preferred over `isinstance(x, types.FunctionType)` in Python?

level: middleimportance: should knowfreq 40%

basics

~10 s

callable(x) asks whether type(x) defines __call__, so it accepts classes, built-in functions, bound methods, functools.partial objects and instances with __call__. types.FunctionType matches only def and lambda objects, rejecting most callables real code passes around.

open as a page

When does a dict of functions beat an if/elif chain for dispatch in Python?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A dict mapping keys to function objects makes branch selection one lookup, keeps handlers independently testable, and turns an unknown key into a loud KeyError instead of a silent fall-through. Keep if/elif when conditions are not equality on one key.

open as a page

Why can you set `f.calls = 0` on a Python function but not on an `int`?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Function objects carry an ordinary instance dict, so arbitrary attributes stick to them just as they do on a normal instance. int and most builtin types have a fixed C-level layout with no dict, so the assignment raises AttributeError.

open as a page