10 Tips to Write Better Functions in Python

By on 19 January 2022

Functions are your primary building blocks in Python. They can be deceptively easy, yet they are profound.

Right off the bat there are some major benefits of using functions:

  • Functions make your code more modular, re-usable and DRY (don’t repeat yourself).
  • This will make your code easier to test!
  • Functions better isolate functionality through scoping (as per the Zen of Python: namespaces are one honking great idea).
  • They have documentation built in through docstrings.
  • They combat the everything needs to be a class bias you sometimes see, which certainly does not hold true, specially when getting anything off the ground.
  • And lastly, their flow is easy to reason about: 1) receive something (input) -> 2) do something (transform) -> 3) return something (output)

So it’s not hard to pitch using more functions in your code, and I am sure most of you already do (hence why I am not covering the syntax in this article, although I might do a follow up article).

So let’s look at what makes for better functions? Here are 10 tips:

  • Every function name is an opportunity to self document your code. You can use various words separated by underscores (as per PEP8 convention). Having multiple words and not putting types in them (e.g. id_list) can greatly increase the readability of your code.
  • As per the Single-responsibility principle, a function should do one thing only. This makes them easier to test and reuse. It’s hard to define a length restriction but going over 15 lines should make you pause and reflect.

    Functions usually grow longer because they are doing too many things. In that case you can abstract behavior in one or more new functions and call those inside the original function. Again, this will make your code easier to test and gives you more opportunities to name each chunk.
  • Keep the function’s interface (that is, the arguments that go into it) small. If you’re defining functions with similar arguments, can you group those arguments into one object (e.g. a namedtuple or dataclass)?

    Also, at that point, a class might be more appropriate. Usually I ask myself if I need to keep some sort of state?

    Also use arbitrary args/ keyword args sparingly, they allow any number of arguments to be passed into your function which might hamper the function’s readability and maintenance because it decreases the strictness of the interface (less boundaries).
  • Check input arguments early and raise an exception if they are off, this will limit your levels of nesting (Zen: Flat is better than nested).

    This also offers a clear contract for the caller of the function (“this is how we are going to play the game”). For more on exception handling, check out this article.
  • An easy win is to define variables as close to where you are using them. Less scrolling, more readability.
  • Python 3 allows for positional-only arguments and keyword-only arguments. Specially the latter can make your code a lot more readable and I have seen it used in FastAPI and SQLModel for example.
  • Adding type hints can greatly enhance the readability of you code and allows tooling to catch bugs earlier. If you’re new to them, start here.
  • Make return types consistent. For example, when returning a sequence, in the “0 items” case, return an empty list instead of None (which is “falsy” too). Type hints can enforce this.

To wrap it up, here are two more tips related to common pitfalls to avoid:

  • First, don’t use “global” in your functions because it lets you access (change) variables in the outer scope. Functions should be “pure”, meaning they don’t have “side effects”. Changing something in the outer scope violates this and can lead to insidious bugs.
  • Secondly, arguments are evaluated once, so using a mutable default argument (e.g. a list or a dict), you might be surprised that the same data structure will be operated on with subsequent calls.

    This is often not what you want, so it’s recommended to use None and make a new list inside the function which creates a new copy upon each invocation. Here is a code example to make this clearer.

I hope this helps you write better functions. If you have any feedback or like to discuss this further join our Slack Community and start a conversation in the #codequality channel.

If you want some dedicated guidance building your dream app end-to-end (MVP ready), we help people reach that goal through our coaching.

And other than that, keep calm and code in Python 🙂

– Bob

Want a career as a Python Developer but not sure where to start?