General concepts

Explicit code

While any kind of black magic is possible with Python, themost explicit and straightforward manner is preferred.

Bad

  1. def make_complex(*args):
  2. x, y = args
  3. return dict(**locals())

Good

  1. def make_complex(x, y):
  2. return {'x': x, 'y': y}

In the good code above, x and y are explicitly received fromthe caller, and an explicit dictionary is returned. The developerusing this function knows exactly what to do by reading thefirst and last lines, which is not the case with the bad example.

One statement per line

While some compound statements such as list comprehensions areallowed and appreciated for their brevity and their expressiveness,it is bad practice to have two disjointed statements on the same line of code.

Bad

  1. print 'one'; print 'two'
  2.  
  3. if x == 1: print 'one'
  4.  
  5. if <complex comparison> and <other complex comparison>:
  6. # do something

Good

  1. print 'one'
  2. print 'two'
  3.  
  4. if x == 1:
  5. print 'one'
  6.  
  7. cond1 = <complex comparison>
  8. cond2 = <other complex comparison>
  9. if cond1 and cond2:
  10. # do something

Function arguments

Arguments can be passed to functions in four different ways.

  • Positional arguments are mandatory and have no default values. They arethe simplest form of arguments and they can be used for the few functionarguments that are fully part of the function’s meaning and their order isnatural. For instance, in send(message, recipient) or point(x, y)the user of the function has no difficulty remembering that those twofunctions require two arguments, and in which order. In those two cases, it is possible to use argument names when calling thefunctions and, doing so, it is possible to switch the order of arguments,calling for instance send(recipient='World', message='Hello') andpoint(y=2, x=1) but this reduces readability and is unnecessarily verbose,compared to the more straightforward calls to send('Hello', 'World') andpoint(1, 2).

  • Keyword arguments are not mandatory and have default values. They areoften used for optional parameters sent to the function. When a function hasmore than two or three positional parameters, its signature is more difficultto remember and using keyword arguments with default values is helpful. Forinstance, a more complete send function could be defined assend(message, to, cc=None, bcc=None). Here cc and bcc areoptional, and evaluate to None when they are not passed another value. Calling a function with keyword arguments can be done in multiple ways inPython; for example, it is possible to follow the order of arguments in thedefinition without explicitly naming the arguments, like insend('Hello', 'World', 'Cthulhu', 'God'), sending a blind carbon copy toGod. It would also be possible to name arguments in another order, like insend('Hello again', 'World', bcc='God', cc='Cthulhu'). Those twopossibilities are better avoided without any strong reason to not follow thesyntax that is the closest to the function definition:send('Hello', 'World', cc='Cthulhu', bcc='God').

As a side note, following the YAGNIprinciple, it is often harder to remove an optional argument (and its logicinside the function) that was added “just in case” and is seemingly never used,than to add a new optional argument and its logic when needed.

  • The arbitrary argument list is the third way to pass arguments to afunction. If the function intention is better expressed by a signature withan extensible number of positional arguments, it can be defined with theargs constructs. In the function body, args will be a tuple of allthe remaining positional arguments. For example, send(message, args)can be called with each recipient as an argument: send('Hello', 'God', 'Mom', 'Cthulhu'), and in the function body args will be equal to('God', 'Mom', 'Cthulhu'). However, this construct has some drawbacks and should be used with caution. If afunction receives a list of arguments of the same nature, it is often moreclear to define it as a function of one argument, that argument being a list orany sequence. Here, if send has multiple recipients, it is better to defineit explicitly: send(message, recipients) and call it with send('Hello', ['God', 'Mom', 'Cthulhu']). This way, the user of the function can manipulatethe recipient list as a list beforehand, and it opens the possibility to passany sequence, including iterators, that cannot be unpacked as other sequences.

  • The arbitrary keyword argument dictionary is the last way to passarguments to functions. If the function requires an undetermined series ofnamed arguments, it is possible to use the **kwargs construct. In thefunction body, kwargs will be a dictionary of all the passed namedarguments that have not been caught by other keyword arguments in thefunction signature. The same caution as in the case of arbitrary argument list is necessary, forsimilar reasons: these powerful techniques are to be used when there is aproven necessity to use them, and they should not be used if the simpler andclearer construct is sufficient to express the function’s intention.

It is up to the programmer writing the function to determine which argumentsare positional arguments and which are optional keyword arguments, and todecide whether to use the advanced techniques of arbitrary argument passing. Ifthe advice above is followed wisely, it is possible and enjoyable to writePython functions that are:

  • easy to read (the name and arguments need no explanations)
  • easy to change (adding a new keyword argument does not break other parts ofthe code)

Avoid the magical wand

A powerful tool for hackers, Python comes with a very rich set of hooks andtools allowing you to do almost any kind of tricky tricks. For instance, it ispossible to do each of the following:

  • change how objects are created and instantiated
  • change how the Python interpreter imports modules
  • It is even possible (and recommended if needed) to embed C routines in Python. However, all these options have many drawbacks and it is always better to usethe most straightforward way to achieve your goal. The main drawback is thatreadability suffers greatly when using these constructs. Many code analysistools, such as pylint or pyflakes, will be unable to parse this “magic” code.

We consider that a Python developer should know about these nearly infinitepossibilities, because it instills confidence that no impassable problem willbe on the way. However, knowing how and particularly when not to usethem is very important.

Like a kung fu master, a Pythonista knows how to kill with a single finger, andnever to actually do it.

We are all responsible users

As seen above, Python allows many tricks, and some of them are potentiallydangerous. A good example is that any client code can override an object’sproperties and methods: there is no “private” keyword in Python. Thisphilosophy, very different from highly defensive languages like Java, whichgive a lot of mechanisms to prevent any misuse, is expressed by the saying: “Weare all responsible users”.

This doesn’t mean that, for example, no properties are considered private, andthat no proper encapsulation is possible in Python. Rather, instead of relyingon concrete walls erected by the developers between their code and others’, thePython community prefers to rely on a set of conventions indicating that theseelements should not be accessed directly.

The main convention for private properties and implementation details is toprefix all “internals” with an underscore. If the client code breaks this ruleand accesses these marked elements, any misbehavior or problems encountered ifthe code is modified is the responsibility of the client code.

Using this convention generously is encouraged: any method or property that isnot intended to be used by client code should be prefixed with an underscore.This will guarantee a better separation of duties and easier modification ofexisting code; it will always be possible to publicize a private property,but making a public property private might be a much harder operation.

Returning values

When a function grows in complexity it is not uncommon to use multiple returnstatements inside the function’s body. However, in order to keep a clear intentand a sustainable readability level, it is preferable to avoid returningmeaningful values from many output points in the body.

There are two main cases for returning values in a function: the result of thefunction return when it has been processed normally, and the error cases thatindicate a wrong input parameter or any other reason for the function to not beable to complete its computation or task.

If you do not wish to raise exceptions for the second case, then returning avalue, such as None or False, indicating that the function could not performcorrectly might be needed. In this case, it is better to return as early as theincorrect context has been detected. It will help to flatten the structure ofthe function: all the code after the return-because-of-error statement canassume the condition is met to further compute the function’s main result.Having multiple such return statements is often necessary.

However, when a function has multiple main exit points for its normal course,it becomes difficult to debug the returned result, so it may be preferable tokeep a single exit point. This will also help factoring out some code paths,and the multiple exit points are a probable indication that such a refactoringis needed.

  1. def complex_function(a, b, c):
  2. if not a:
  3. return None # Raising an exception might be better
  4. if not b:
  5. return None # Raising an exception might be better
  6. # Some complex code trying to compute x from a, b and c
  7. # Resist temptation to return x if succeeded
  8. if not x:
  9. # Some Plan-B computation of x
  10. return x # One single exit point for the returned value x will help
  11. # when maintaining the code.