Level up your Python game

This post if for the artists who have been dipping their toes in the water of Python development and are considering taking it to the next level. We will cover some of the points that differ between an artist/amateur level of development and an entry TD level.  I’m covering quite a few subjects in this article so go though it at your own pace, there is no need to read everything in one seating.

Follow the rules

Once you start writing code at a larger scale, it becomes important to follow the rules.

You may have to get back to your code months or years from now, maintain it when someone finds a bug in it, and it might get modified by other TDs or developers at the studio. Your codes will also need to be reliable, and easy to read. The best way to achieve that is to follow a set of rules.

What the rules are differs depending where you are working. Each studio might be following a slightly different set of rules, and have slightly different coding styles.

There are some pretty generic rules that apply at most studios though, and generic python rules, so I’ll do a quick overview of these.

Most of it comes from PEP8, which defines a lot of coding style rules for Python.

Naming conventions

Following naming conventions when coding is just as important as following naming conventions when doing VFX work: If you don’t do it it will be a mess.

I would say the most important of the naming rules is: Be explicit and clear with functions and variables names.

Consider the example below:

def pick(what, n=None):
    if n is None:
        n = nuke.selectedNodes()

    for i in n:
        if i.Class() not in what:
            i.knob("selected").setValue(False)

While a very easy function, it would be so much easier to read with explicit names:

def filter_by_class(classes_to_keep, nodes=None):
    if nodes is None:
        nodes = nuke.selectedNodes()

    for node in nodes:
        if node.Class() not in classes_to_keep:
            node.knob("selected").setValue(False)

In general, it’s recommended to name functions and methods so that they start with a verb. PEP8 recommends naming them in lower_case_with_underscores although the Foundry and some other vendors tend to do mixedCase. Pick one, and stick with it. Here are a few bad function names and their better counterpart: coffee > make_coffee, mail_getter > get_mail, millionaire > get_rich.

For variables, I think most places stick with lower_case_with_underscores and have the variable name represent what the variable contains: selected_nodes, image_width, coffee_size, etc.. avoid generic names or abbreviations that aren’t universally recognized, even if they may seem obvious to you: color_correct_node is better than cc.

Classes tend to use CapitalizedWords and also represent clearly the type of object they represent.

class CoffeeCup(object):
    def __init__(self, size):
        self.size = size
        self.level = 0
        print "Taking a clean mug out, it's empty for now."

    def __repr__(self):
        return "Coffee cup is {}% full".format(int(float(self.level)/self.size*100))

    def fill(self):
        self.level = self.size
        print "Filling Coffee Cup"

    def drink(self, large_gulp=False):
        print "Drinking some coffee"
        gulp_size = 20 if large_gulp else 10
        self.level = max(self.level - gulp_size, 0)


def take_break(number_of_cups_to_drink, in_rush=True, very_tired=False):
    cup_size = 250 if very_tired else 120
    coffee = CoffeeCup(cup_size)

    cups_left_to_drink = number_of_cups_to_drink
    while cups_left_to_drink:
        coffee.fill()
        while coffee.level:
            coffee.drink(in_rush)
            print coffee
        cups_left_to_drink -= 1

take_break(2, False, False)

Fail early, fail gracefully

Your code will run into errors. Often.

A lot of novice developers (myself included at the beginning) tend to just wrap whole functions in try…except statements. While this will indeed suppress the errors, it will also sometimes make it extremely hard to debug your code, as it will hide errors away, causing the code to error somewhere else, or do nothing at all.

Where errors are possible or likely on a function that takes external arguments, ensure the required arguments are provided, and if not stop execution and raise the error right away, providing a useful error message. Example: “Expected int argument for coffee cup size, got str”

When using try…except statements, use them on as few lines as possible (ideally just the one likely to fail) and make sure the except is looking for specific error types rather than broadly catch every possible exception.

Setup Pylint

Some tools, like Pylint can analyze your code for you, and ensure you’re following all the rules. It’s pretty thorough and can be configured to follow specific rules for your project. I learned a lot of rules from using Pylint, and while I decided to ignore some of them (it’s pretty strict by default), it was a good learning experience to have some of these errors pointed out to me so I could Google search why this is a bad pattern.

Some light reading

I like The Little Book of Python Anti-Patterns, and recommend reading through it (as well as PEP8 or the Google Python style guide) to learn more about clean code.

Use a solid IDE

Typing all your code in Notepad or directly in Nuke? Stop!

There are some very solid Python IDEs (integrated development environment) which are editors dedicated to writing code, and preferably python code.

They will help you save time by providing auto-completion, can understand some of the code to highlight errors and give warnings, and will help you enforce the rules from above!

My personal favorite is PyCharm, which is fully dedicated to Python coding, and has a free community version!. Some people swear by Sublime as well, but I find it less adapted for python, though it might be a better option if you’re also coding in other languages. There are plenty of other IDEs too, a google search will give you access to many options.

Use version control

I remember my early code, where I would save incremental versions of a python script (just like I would do with a nuke script), or would occasionally copy my whole python folder into a “backup” folder, with a timestamp. What a mess it was. While that sort of works for (very) small scale projects, it quickly becomes totally unmanageable, and should not be an option when your primary responsibility is to write code.

There are version control systems, that record changes to files over time, and allow for “checkpoints” and full history of every change that has ever happened. Coupled with code comparison (diff) tools, it’s a winning combo.

One of the most commonly used version control system today is Git with multiple implementations that allow you to save your code “on the cloud”. GitHub is probably the most famous, and lets you save your repository for free as long as it’s public. It also has paid options if you would like to make your code private. GitLab is similar, but free even for private projects. It also has some paid features, but you probably won’t need them until later.

For learning Git, there are plenty of tutorials online, but the basic idea is that all your code lives in a repository. You can think of it as a managed directory (it actually is a directory on your hard drive). Usually you’ll have at least 2 repositories, one local, and one remote (on the cloud or some server somewhere), though nothing stops you to work locally only (in which case you don’t even need a github or gitlab account). There is main branch, usually called master, which represents the “state of the art” version of your code. When you want to add a feature of fix a bug, you check out the code (basically make your local copy match the remote copy), make a new branchand make your changes. You can think of your branches as parallel versions. If it was a comp, you could think of it as the master branch representing the last version you sent for review, then you decide to make a red version and a blurry version to send as options. With Git, you’d make 2 branches, one for the red version, one for the blurry version.
Once you are done with some changes, you commit your code. This is like hitting save, except you can do it as many times as you want in your branch, and you will have access to every single revision individually later on if needed. The code goes into the branch on which you were currently working. If you want to save it on the cloud as well, you need to push the code to the cloud.
Eventually, once your features or modifications are tested and ready, they can be merged back into the master branch, at which point they become the new state of the art version of the code.

That’s highly simplified, but that is 95% of the use I make of git: checkout, branch, commit, push, merge. You can do some reading about it here: WTF is Git? or Git and Github for beginners.

The great news is that if you are using PyCharm, it has a pretty tight Git integration, and you probably don’t ever need to use Git in command line (though I had a few times where it lost its bearings and I had to go back to command line to fix it, but these were special cases and user error mostly).

Write Documentation

The next step towards clean code is to ensure you’re not the only person who can use the code. A good step for that is to write documentation.

The first level of documentation I like to do is to put comments in my code explaining my reasoning. It’s not actual documentation, but it can help people going through my code understand why I did some things a certain way. However that probably isn’t enough.

While you could write a lot of documentation manually, there are also some libraries that can generate documentation automatically for you, assuming you’re following conventions, and that you add a few things in your code. Two of the most common ones are Sphinx and Epydoc (used to generate the Nuke python API reference). These tools can run through your code, a little bit the same way Pylint does, and generate documentation for it. These tools also use what we call docstrings to complete the documentation.

Docstrings are great, as they can be read either by these automated tools or by a human reader. They can also be read using the help() python function. You probably ran into them in the past without really knowing how powerful they were. Docstrings are little pieces of documentation written directly in the code!

def my_function():
    """ This is a docstring! It is surrounded by triple quotes. """
    return True

It’s easy to read as you read my code, and if I was to generate documentation through Sphinx it would look something like this:

A lot of information can be added in docstrings, but the most common is to document what are the arguments of the function, their type, and what will the function return. Of course docstrings aren’t limited to functions, they can also be used to document classes or even entire modules, and could get extended to be really long and complete if necessary. I’ve seen code where the docstrings are longer than the code itself! There are a few different schools for the docstring syntax, the two main ones being reStructured Text, the other one being Google style docstrings.

I’m personally using reStructured Text (also known as reST or rst), but again, follow the conventions established at your studio.

A typical rst documented piece of code looks like this:

class CoffeeCup(object):
    """ Represents a Coffee cup.
    
    The cup size must be defined on initialization. It is originally empty and need to be filled.
    """
    def __init__(self, size):
        """ Initialize the Cup

        :param size: Size of the cup, in ml
        :type size: int 
        """
        self.size = size
        self.level = 0
        print "Taking a clean mug out, it's empty for now."

    def __repr__(self):
        return "Coffee cup is {}% full".format(int(float(self.level)/self.size*100))

    def fill(self):
        """ Fill (or Refill) the cup entirely """
        self.level = self.size
        print "Filling Coffee Cup"

    def drink(self, large_gulp=False):
        """ Drink a gulp of Coffee. (10ml or 20ml)
        
        :param large_gulp: Take an bigger gulp (20ml) of Coffee
        :type large_gulp: bool
        """
        print "Drinking some coffee"
        gulp_size = 20 if large_gulp else 10
        self.level = max(self.level - gulp_size, 0)


def take_break(number_of_cups_to_drink, in_rush=True, very_tired=False):
    """ Take a break and drink some coffee
    
    :param number_of_cups_to_drink: How many cups of coffee to drink
    :type number_of_cups_to_drink: int
    :param in_rush: Whether or not you are in rush to take the break.
    :type in_rush: bool
    :param very_tired: Whether or not you need BIG cups of coffee.
    :type very_tired: bool
    :return: Returns the total amount of coffee drunk
    :rtype: int
    """
    cup_size = 250 if very_tired else 120
    coffee = CoffeeCup(cup_size)
    total_amount_of_coffee = 0

    cups_left_to_drink = number_of_cups_to_drink
    while cups_left_to_drink:
        coffee.fill()
        while coffee.level:
            coffee.drink(in_rush)
            print coffee
        total_amount_of_coffee += cup_size
        cups_left_to_drink -= 1
    return total_amount_of_coffee

take_break(2, False, False)

You can read more about documenting here: Documenting your project using Sphinx

The other nice thing about docstrings is that both PyCharm and Pylint will understand them, and warn you if you then try to pass wrong arguments to functions.

User Interface and Experience

Also known as UI/UX. User Interface is the way users interact with your code. Here we’ll focus on Graphical User Interfaces. UX is a broader term, but generally represents the whole experience of using your code, like how pleasant or frustrating it is to use your product. For the scope of this article, we’ll simplify the definition and say good good UI is to make things pretty, good UX is to make things easy and pleasant to use. I find both usually go together.

For making UIs in VFX, the most common library is Qt. It’s used by Nuke, Maya, Houdini, etc.. Qt itself is using c++, but it has two Python implementations: PyQt and PySide.  Right now is a bit of an awkward period, as the VFX reference platform last year switched from Qt4 to Qt5, which means applications like Nuke 10 use PyQt4/PySide, while Nuke 11 uses PyQt5/PySide2. Of course they are not 100% compatible with each other, so you need to either code towards a specific version or use the Qt Shim which is a little utility some VFX guys wrote that bridges the differences between the different versions, that you can write code once, and it will work with any available version. The shim is using the syntax of PySide2, so one of my most frequently visited website is the PySide2 documentation.

Qt is using widgets (Nuke knobs are pretty much straight Qt Widgets) to represent every graphical element. You might have a checkbox widget or a dropdown menu widget (called combobox) and you put them together to assemble your UI. There are a lot of default widgets that can be used, and you can even make your own from scratch once you’re familiar with it. I’m not going to teach Qt here, that would be huge, but a good intro site is Zetcode (It’s PyQt5 rather than PySide2, but they are almost similar).

In terms of making good UI/UX combinations, I compiled a few generic advises (by the way, these also apply to making gizmos):

  • Controls should be as explicit and intuitive as possible. It should work as closely as possible the same way it does in the application the users use most often. It it follows the same logic, it will be easy for the users to figure out the usage. If you have to explain to them how to use your tool, it’s probably not instinctive enough.
  • Use the right kind of widgets for the job. While we may not think about it, we have been using GUIs for years, and we are getting used to a specific way to interact with software. Don’t use a dropdown menu where a checkbox makes more sense, and vice versa.
  • Set useful default values. If most values are pre-filled with their most useful values, it will be easy for the user to customize what they need, rather than having to go and set each setting manually.
  • Label your widgets and set tooltips. Ideally the users should not even need to read the tooltip, but it’s always good to give more detailed info.
  • Too many options is not better than too few. Facing a wall of settings or too many widgets at once can be scary for the user. Expose the most important widgets only, or organize the UI into logical sections. Having too few options can also be detrimental. It’s all about balance.

Going even further

All of this should keep you busy for a while. Transitioning to using these tools doesn’t happen overnight, so don’t stress yourself trying to ingest this whole article at once, take it piece by piece and learn the tools in the order you wish.

If this was too easy for you though, remember that next year Python3 will become the new standard, as Python2 will reach it’s end of life, so you can get started flexing those Python 3 muscles.

You could also do a bit of reading about Test-Driven Development, and how to apply it to Python.

By |2018-09-14T04:06:37+00:00September 12th, 2018|General, Python, Tutorials|1 Comment

One Comment

  1. Mokhtar Ebrahim October 2, 2018 at 12:19 pm - Reply

    I liked this naming convention.
    Thanks for the info!

Leave A Comment