November 2019

Volume 34 Number 11

[The Working Programmer]

Python: Functions

By Ted Neward | November 2019

Ted NewardWelcome back, Pythonistas. This will be, as many of you know, the last article in this series, but more on that later. In the previous part of this series, I examined how Python flow control works—branching, looping and exception handling—but one of the most significant ways to control (and reuse) code is with the time-honored approach of bundling it into a named block of code that can be invoked repeatedly. I speak, of course, of the traditional function. (Python also supports classes, of course, but much of that should be easy to pick up once you understand the function syntax.)

Predefined Functions

First things first: Unlike many of its object-oriented contemporaries, Python supports (and encourages) top-level functions. In fact, you could program Python entirely in a procedural fashion, if you wanted. (This is partly why Python often gets labeled a “functional” language; more on that later.)

Python has a number of built-in functions defined as part of the core environment, such as the print function I’ve already discussed. In addition to some core math functions (abs, divmod, max, min, round and pow); some type-conversion functions that transform a variable from one type to another (bool, bin, chr, ord, float, int, hex and oct, among others); and some convenience methods to help construct various types (dict makes it easier to create dictionary instances, and list does the same for lists); Python also has a number of “meta” functions that operate on the language or its elements directly. The eval function, for example, takes a string containing Python and executes that code. Similarly, the compile function takes a string and transforms it into a compiled object for future execution using either eval or its cousin, exec.

But possibly the most “meta” of the Python functions is the dir function, which is named after the shell command of the same name. It returns a list of the names available “inside” the passed object, or the top-level scope if no object is passed. Thus, if you fire up the Python REPL and simply type “dir” into the prompt, Python will respond with all of the global functions and classes that are currently loaded. This is a ridiculously powerful tool that’s great to use when exploring new libraries. This reflective vision of the running environment can also be used to examine all of the globals in the module via the globals function, and later, when I start defining some functions, I can examine all of the local variables in the current scope using the locals function.

Function Basics

Defining functions in Python is pretty much exactly the way it is in any other programming language: You define a block of code (properly indented; this is Python, after all) with a name, and the function can take a number of parameters and return a value, like so:

def quest():
  print("You must now cut down the tallest tree in the forest...")
  print("With... A HERRING!!!!!")

Invoking a function is, as might be assumed from the various print examples used thus far, pretty much exactly as it is in other languages—the name, followed by parentheses enclosing any parameters, and off we go:

quest()

So far, so good. If you want the function to take parameters, you include one or more names inside the parentheses of the function definition. Any value to be returned is given using the ubiquitous “return” keyword:

def theKnightsWhoSayNi(gift):
  if gift == "shrubbery":
    return ("You must now cut down the tallest tree"
      "in the forest... with... A HERRING!!!!")
  else:
    return "You must bring us... A SHRUBBERY!!!"

(Notice that when two string literals are adjacent to one another and parenthesized, Python will concatenate them into a single string.) In keeping with Python’s dynamically typed nature, any value can be passed for a given parameter, so it’s generally a good idea to make sure users know how to use your function correctly; in Python this is done by providing a documentation string (“doc-string”) string literal as the first non-commented line of code inside the function, like so:

def theKnightsWhoSayNi(gift):
  """In order to continue your quest for Camelot, you must
  bring the Knights gifts. Do not mess with these guys, for
  they say 'Ni' at the slightest provocation!"""
  if gift == "shrubbery":
    return ("You must now cut down the tallest tree"
      "in the forest... with... A HERRING!!!!")
  else:
    return "You must bring us... A SHRUBBERY!!!"

The triple-quoted string is what’s often called a “here-doc” multiline string literal—it will capture everything inside the pair of triple quotes, including whitespace, making it useful for documentation purposes. Additionally, Python will do something quite special to this string—it will capture it into a property on the function itself, and make it discoverable at runtime via the “__doc__” property name. To see what this means, try calling this bit of Python after the function has been defined:

print(theKnightsWhoSayNi.__doc__)

Lo and behold—helpful documentation about the function is printed to the command line! And it turns out that this “__doc__” is not the only interesting thing on the function—if you do a dir on the function name, a whole slew of interesting things (properly called “attributes”) appear:

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Clearly, Python is doing a fair bit of interesting organization and structuring of functions under the hood.

Customizing Arguments

In some cases, you want arguments to be optional, meaning you want to provide a default value to the parameter in the event the caller doesn’t provide one. This is done by providing said value in the function definition:

def theKnightsWhoSayNi(gift = None):
  """In order to continue your quest for Camelot, you must
  bring the Knights gifts. Do not mess with these guys, for
  they say 'Ni' at the slightest provocation!"""
  if gift == "shrubbery":
    return ("You must now cut down the tallest tree"
      "in the forest... with... A HERRING!!!!")
  else:
    return "You must bring us... A SHRUBBERY!!!"

Now the function can be invoked with either one parameter or none. None, by the way, is the closest equivalent Python has to null or nil in other languages.

Python also has what they call “keyword parameters,” which allows you to use named parameters in function calls instead of (or in addition to) positional ones. This means that if you have a function defined with a few default values, like so:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
  print("-- This parrot wouldn't", action, end=' ')
  print("if you put", voltage, "volts through it.")
  print("-- Lovely plumage, the", type)
  print("-- It's", state, "!")

You can then call the function in a variety of different ways, either by using positional arguments (the normal way) or by keyword arguments, like this:

parrot(1000)
parrot(type='Turkish Red', action='caw', voltage=1000)

In any case, all non-default parameters must have a value, and keyword arguments can only appear after all positional arguments do. If you prefer to pass a dictionary for the arguments, put two asterisks in front of the dictionary parameter at the point of call:

d = {"voltage": "a million", "state":"bleedin' demised"}
parrot(**d)

Python also allows for arbitrary argument lists, akin to the “params” modifier from C#, if the parameter is declared with an asterisk preceding its name. This will then capture all positional arguments after that point into a single list for processing, like so:

def order(bread, sides, *fixings):
  print("Making you a Spam-on-", bread)
  print("  with", sides)
  for f in fixings:
    print("  and", f)
  print("Enjoy!")
order("white", "pickles")
order("wheat", "radishes", "pickles", "chips", "pint o' lager")

Nothing prevents you from writing all your functions to take one arbitrary argument list, just as you can do in ECMAScript, but most of the time that’s not idiomatic Python and will likely get somebody to chase you with a herring.

Anonymous Functions

Python also supports functions-as-first-class-citizens out of the box, in terms of passing a function as a parameter, such as what you might use in the map global function, which executes a function over each element of a list:

items = ["bread", "eggs", "milk"]
def spametize(item): return "spam"
spamCafeOrder = map(spametize, items)
print("We need to go to the market and get:")
for it in spamCafeOrder: print(it)

But when working with functions, it’s often useful to define a function anonymously. In Python, you do this via the “lambda” keyword, which replaces the “def” and function name in the definition:

def add(lhs, rhs): return lhs + rhs
def makeAdder(lhs):
  return lambda rhs: add(lhs, rhs)
addFive = makeAdder(5)
print(addFive(12)) # prints 17

Unlike other languages, Python insists that lambda functions be restricted to small-use scenarios: A given lambda-defined function can only consist of a single expression (such as the add call in the preceding example).

Wrapping Up

Python functions are among the core building blocks of the platform, and every bit as important as classes and objects. Unlike some of the “traditional” object-oriented languages (C#/C++/Java), Python not only doesn’t fear global functions, it embraces them as a key design tool.

Because Python has lambdas and first-class functions, many people will categorize Python as a functional language (on the order of languages like Haskell or F#), but I do not. Python lacks a number of the features that a traditional functional language will have (like partial application of functions or immutability by default). Although you can pass functions around as objects, you can do the same thing in a number of other languages, so if Python is functional, so is C. And, trust me, nobody wants to go there.

Farewell, All

As you have either heard or read, this installment marks the end of this column, and this magazine. To say that this makes me emotional is quite the understatement; 20 years ago, I was the feverish consumer of every issue of Microsoft Systems Journal, drinking up every bit of wisdom and lore it offered like the proverbial thirsty man in the desert. Then I got to write for the magazine, and I cannot tell you how amazing that felt; it was a bucket-list item checked off, to be sure.

For close to a decade, you’ve been giving me the privilege of exploring topics with you, and I hope you’ve enjoyed that ride as much as I have. From Mongo and Cassandra to Naked Objects, with stops at MEAN and Multiparadigmatic Design along the way, I’ve had a ton of fun learning and teaching and exploring and, even, when the mood called for it, having a little fun (the LOLCODE article will be, by far, my favorite column installment I will ever write).

For me and the other MSDN Magazine authors, our journey with you through the magazine ends here. But the journey as a whole continues: All of us authors, we’re out there, in the world, and we’re just as approachable for questions and discussion as we were here. If you find one of us at a conference, don’t be a stranger. Come on up, say hi, introduce yourself and tell us what you’re working on. The two-way street that this magazine created between us isn’t shut down when the magazine shutters its doors—it just needs to take a different form.

All good things in time must come to an end, and so have I. Please, as you go about your career and life, always remember:

Happy coding!


Ted Neward is a Seattle-based polytechnology consultant, speaker and mentor. He has written a ton of articles, authored and co-authored a dozen books, and speaks all over the world. Reach him at ted@tedneward.com or read his blog at blogs.tedneward.com.

Thanks to the following technical expert for reviewing this article: Harry Pierson
Harry Pierson was a founding member of Windows runtime and xlang and product manager for IronPython. He spent 10+ years on the OS team, including Windows core and the Midori research project. He is currently chief architect for NGD based in Seattle.