Anatomy of a Python Function
MP 160: When we're discussing functions in Python, what do we call the different parts?
I've been reflecting on typing in Python recently. I haven't used type hints much, because all my programs have worked fine without them. But typing in Python has matured quite a lot since it was first introduced. I originally came to Python from C and Java, and I was quite happy not to have to specify types for a while. I wasn't eager to jump back into declaring types when they were first introduced to Python, but I'd like to start using them where they are helpful.
I've been wanting to write about types and some of the new type checkers that have been released over the last year or so. In starting to write some drafts, I realized that the names I typically use for parts of a function aren't consistent with how the wider community talks about functions. It also turns out there's some variety in the names that people use for the different parts of a function. We can usually understand each other well enough, but are some names better than others? Naming things is hard, but that also means names are worth talking about.
Two example functions
Let's consider two functions. This first one is pretty much the simplest function we can create:
def greet(): print("Hello.")
The function greet() has a name, and a single line of code. That's about it.
Now let's look at a function with a few more parts:
def welcome(name: str) -> str: return f"Welcome, {name}!"
The function welcome() has more of the parts we see in real-world functions. It has a parameter with a type hint, the return type is specified, and it returns a value.
Let's figure out how to name the parts of these functions.
A shared vocabulary
Let's start with the parts of a function that we most agree on. All the indented lines constitute the function body. Whether it's a single-line function or a thousand lines, I've never seen disagreement about what the term function body refers to.
The function definition consists of everything from def through the end of the body. This is where my usage differed from most people in the Python world. I used to think the definition was only the parts from def through the colon. I don't know where I got this idea; I might have thought that def started the function definition, and it ended at the colon. Regardless, my original understanding was not consistent with the rest of the community.
More varied terms
Here's where things get more interesting. What do you call this part of a function?
def welcome(name: str) -> str:
I've heard a number of terms: signature, header, and declaration. Let's look at each of these in turn.
Function signatures
A function signature actually has the clearest meaning, because it has an implementation. PEP 362 describes a function's Signature object, which was implemented almost 15 years ago. The signature of a function consists of the parameters and the return annotation.
Here's what that looks like for welcome():
>>> def welcome(name: str) -> str: ... return f"Welcome, {name}!" ... >>> from inspect import signature >>> sig = signature(welcome) >>> str(sig) '(name: str) -> str'
The signature of welcome() is just (name: str) -> str. It doesn't include def, it doesn't include the function name, and it doesn't include the colon.
Here's the signature object for greet():
>>> def greet(): ... print("Hello.") ... >>> sig = signature(greet) >>> str(sig) '()'
The signature of greet() is empty!
Clearly, a function's signature is something useful to be able to name, discuss, and work with in code. But it doesn't represent that full first line of a function.
Function declaration
Some people call that first line the function declaration. I don't see any major issue with this in the Python world, because function declarations don't have a well established meaning in Python.
However, there are languages where you can declare a function at one point in a program, and then provide a definition for the function later on. That initial declaration is used to let the compiler know what the function's signature will be. People who come from languages where you can declare a function separately from defining it may be thrown off a bit by using the term declaration.
Function header
I like referring to that first line as a function header. This term doesn't have any other specific meaning in Python. I think I'll use this term from now on to refer to "everything in a function that's not the body" or "everything from def to the colon."
The term function header doesn't seem to have a specific meaning in most other languages that I'm aware of, except Java. In Java, there's a MethodHeader, which refers to specific parts of a method (a function associated with a class). However, I don't think this has quite as strong of a connotation as the term declaration.
Discussing types
Why does it matter what we call these parts? Imagine someone uses welcome(), but they make a mistake when calling the function:
def welcome(name: str) -> str: return f"Welcome, {name}!" welcome("Eric", "Willie")
The person has called welcome() with two names, instead of just one. Let's see how a type checker responds:
$ uvx ty check welcomer.py error[too-many-positional-arguments]: Too many positional arguments to function `welcome`: expected 1, got 2 --> welcomer.py:4:17 | 2 | return f"Welcome, {name}!" 3 | 4 | welcome("Eric", "Willie") | ^^^^^^^^ | info: Function signature here --> welcomer.py:1:5 | 1 | def welcome(name: str) -> str: | ^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | return f"Welcome, {name}!"
The type checker generates a fair bit of output for a small mistake, but what we're interested in is the highlighted line:
def welcome(name: str) -> str:
If we're going to talk about type checkers, we probably need a name for this snippet that's displayed in the output. We can't just call it "the first line of the function", because many times that first part of the function is broken up across several lines.
I'm going to call that the function header from now on. I'd be just as happy to hear someone refer to it as the declaration. I'd be less supportive of calling it the signature, because the signature already has a specific meaning in Python. (It's interesting to note that ty currently refers to this as the function signature in the output above.)
Conclusions
This post is a followup to an interesting conversation on Mastodon. If you're curious to hear a few other takes on how we talk about functions, it's a fun read.