Ramblings about rigging, programming and games.

New Python 3 features

April 20, 2021

Maya 2022 finally has support for python 3 and I've been using it quite a lot these past few months so I figured I would shed some light on all the features I've enjoyed using so far.
And because Python 2 isn't quite dead yet, I'll link to alternatives or backports that you can use for python 2 and let you maintain a python 2 and 3 compatible code base.

You'll see a common trend with all of these features, they greatly help writing more readable and self documenting code. If you're in a situation where can use them, I highly recommend that you do.

Type Hints

Python 3 comes with a new syntax to assign a type to your variables.
Adding type hints doesn't have any impact on the runtime performance of your code. It's only there for readability and to be parsed by type checkers.
They are also entirely optional, Here's how they look:

a: int = 0
b: str = "some string"

def above_zero(value: int) -> bool:
 return value > 0

Type hinting is amazing for a few reasons:

For type hints to actually be useful, you need something to enforce and validate them. This is the job of a Type Checker.
I personally use mypy but pyright looks like an interesting alternative. If you use PyCharm, it seems have its own type checker built-in.

Note

While the type hint syntax is new to python 3, it's possible to use a comment based syntax for python 2.
From what I've seen, mypy, pyright and pycharm seem to all be able to use this alternative syntax.

Converting an existing codebase to use static typing can be overwhelming, mypy recommends to start small.
If you're starting fresh however, do yourself a favor and use a type checker before you even write your first line of code.

The quality of my code has improved significantly these past months both in terms of design and readability. Proper static typing was one of the reasons why. It strongly discourages me from writing unreliable code and encouraged me to create more small and meaningfull classes, leading nicely to the next point.

Dataclasses

Info

The python2 compatible implementation of the dataclasses is attrs.
Python3's dataclasses actually come from the attrs library. Their syntax differs a bit and attrs has a few more features.
If you use the first party dataclasses and are going through attrs' documentation, read @dataclass when you see @attr.s and field() when you see attr.ib().

Dataclasses makes writing classes a lot less cumbersome than it used to be.
They are a way to cut boilerplate code by automatically implementing a lot of "dunder" methods for you. They make your code more readable and more useful.

You only need to define a class by its name, attributes and specific methods.
The @dataclass decorator implements __init__, __eq__, __repr__ automatically for you as well as all those other methods you were not implementing but should have. This means that dataclasses can automatically be compared, sorted, etc. with absolutely zero effort on your end.

attrs' documentation has a great overview of why you would want to use dataclasses as opposed to any other alternative.

One of the ways I've been using dataclasses is by making multiple constructors for my objects.
This is something I've picked up when learning Rust and I felt that dataclasses were encouraging me to do the same in python.

from dataclasses import dataclass

@dataclass
class Vector3:
    x: int
    y: int
    z: int

    @classmethod
    def new(cls) -> Vector3:
        """Create a new 'zero' vector."""
        return cls(0, 0, 0)

    @classmethod
    def from_sequence(cls, sequence: Sequence) -> Vector3:
        """Create a vector from any sequence of length 3."""
        if len(sequence) != 3:
            raise ValueError(
                "A Vector3 can be only constructed from a sequence of length 3."
            )
        return cls(*sequence)

# These are all valid ways of instantiating the Vector3 class
Vector3(0, 1, 2)  # using the default constructor
Vector3.new()
Vector.from_sequence([0, 1, 2])  # instantiating from a list
Vector.from_sequence((0, 1, 2))  # and now from a tuple

If you have access to them, you should likely use dataclasses by default, they'll make your life easier.

Protocols

Info

Maya currently uses python 3.7 and Protocols are a 3.8 feature. To use them you need to install the typing_extensions package, which also has python 2 support.

If you use python 3.8+, Protocols are available in the typing module.

This feature might not be relevant for everyone but I really like it and it pairs really well with static typing.
Protocols are a way to define a consistent interface for multiple classes without using inheritance.
Some other languages call them Interfaces, Rust calls them traits.

When you define a Protocol, you just create a class with empty methods that have to be implemented in the classes that implement the protocol.

I'm taking the next example straight from this article which goes more in depth on the topic than I will.

from typing import Protocol

class Flyer(Protocol):
    def fly(self) -> None:
        """A Flyer can fly"""

class FlyingHero:
    """This hero can fly, which is BEAST."""
    def fly(self):
        # Do some flying...

class RunningHero:
    """This hero can run. Better than nothing!"""
    def run(self):
        # Run for your life!

class Board:
    """An imaginary game board that doesn't do anything."""
    def make_fly(self, obj: Flyer) -> None:   # <- Here's the magic
        """Make an object fly."""
        return obj.fly()

def main() -> None:
    board = Board()
    board.make_fly(FlyingHero())
    board.make_fly(RunningHero())  # <- Fails mypy type-checking!

if __name__ == '__main__':
    main()

Note that we never explicitely define that Bird and Human implement the HasWings and HasLegs Protocols, it's implicit because those classes implement all the methods from the Protocol.
I find this quite awkward and you won't get warned that a class implements a protocol incorrectly. Any slight mistake means the protocol isn't implemented.
Luckily, it's possible to check for this at runtime with the isinstance and issubclass methods. If you use unit tests you can simply write a test like this:

from typing import runtime_checkable


@runtime_checkable # <- necessary to use issubclass and isinstance against the protocol
class Flyer(Protocol):
    def fly(self) -> None:
        """Make the object fly."""

class FlyingHero:
    """This hero can fly, which is BEAST."""
    def fly(self):
        # Do some flying...

class RunningHero:
    """This hero can run. Better than nothing!"""
    def run(self):
        # Run for your life!

def test_FlyingHero_implements_Flyer():
    assert issubclass(FlyingHero, HasWings)  # <- This test passes

def test_RunningHero_implements_Flyer():
    assert issubclass(RunningHero, HasWings)  # <- This test fails

F-strings

Info

I have not used a python 2 implementation of f-strings yet but future-strings looks promising.

F-strings are a new way to format strings that is way more readable than previous methods because you are able to embed variables directly into your strings.
Your eyes don't need to constantly jump from the beginning and end of the line to make sense of the strings.

This means your ugly maya.cmds calls will go from this

control = cmds.createNode("transform")
blendshape = "blendShape1"

cmds.connectAttr(
    "{}.translateX".format(control),
    "{}.some_shape".format(blendshape),
)

To this:

control = cmds.createNode("transform")
blendshape = "blendShape1"
cmds.connectAttr(
    f"{control}.translateX",
    f"{blendshape}.some_shape",
)

Enums

Info

The Python 2 implementation of this that I've used is aenum

Enums are a way to define constants and give meaning to otherwise abstract values.

The Enum class can be used to map a name to anything, for example:

from enum import Enum

class Color(Enum):
    red = (1, 0, 0)
    green = (0, 1, 0)
    blue = (0, 0, 1)

The IntEnum is useful to map a name to an int, making it a very good way to interface with maya's enum attribtues. Say you want to change the interpolation type of a keyframe on a remapValue node here's what you'd do:

from enum import IntEnum

class Interpolation(IntEnum):
    none = 0
    linear = 1
    smooth = 2
    spline = 3

remap_value = cmds.createNode("remapValue")
cmds.setAttr(f"{remap_value}.value[0].value_Interp", Interpolation.smooth)

In fact, here's a simple Keyframe class I've written recently that abstracts all the relevant information:

from enum import IntEnum
from dataclasses import dataclass

@dataclass
class Keyframe:
    """Keyframe class meant to work with maya's remapValue and remapColor nodes."""

    class Interpolation(IntEnum):
        none = 0
        linear = 1
        smooth = 2
        spline = 3

    x: float 
    y: float
    interpolation: Interpolation

remap_value = cmds.createNode("remapValue")
keyframe = Keyframe(0, 0, Keyframe.Interpolation.smooth)
cmds.setAttr(f"{remap_value}.value[0].value_Position", keyframe.x)
cmds.setAttr(f"{remap_value}.value[0].value_FloatValue", keyframe.y)
cmds.setAttr(f"{remap_value}.value[0].value_Interp", keyframe.interpolation)

Note

in this example, I define the Interpolation class inside of the Keyframe class as a way to namespace it. I'm not sure if that's good practice but I find it makes it very explicit that the Interpolation class is only meant to be used with the Keyframe class and not something else.

Back to top