New Python 3 features
April 20, 2021Maya 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:
- It's a great way to write self documenting code.
It simply removes a lot of guess work when reading through some code and makes it much easier for multiple people to work on the same codebase. - It improves code completion/inspection a lot.
Without type hints, your editor has no way of knowing the types of a function arguments and can't give you any relevant completions for those. The same is true for what a function returns Good type hinting fixes all that. - It prevents a lot of bugs before they even happen.
Due to the dynamic nature of python, a lot of variables tend to have different types based on the context. If you're not checking their type at runtime, you'll end up calling the wrong methods on the wrong types.
AttributeError: <SomeType> has no attribute <whatever>
is an error you'll won't be seeing much when you get into static typing. Your editor will let you know about those errors before you even run your code.
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
andfield()
when you seeattr.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.
- Your default constructor (the
__init__
, automatically implemented by the dataclass) doesn't make any assumptions about the "default" value of the arguments. - The
new
constructor returns an object with the most sensible default values. - You can add as many constructors as you want and give them a clear name as to what they do.
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 theKeyframe
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 theInterpolation
class is only meant to be used with theKeyframe
class and not something else.