Style Guide¶
Explains the code style and conventions used in this SDK.
The Cinema 4D Python SDK follows the PEP 8 - Style Guide for Python Code in its
code examples and documentation, and adopts the naming and execution conventions of the Cinema API. This guide documents
the most important conventions and deviations from PEP 8 that might not be obvious to users, and the practices required
to run safely alongside other scripts and plugins. SDK contributors should also follow py_sdk/AGENTS.md when adding
examples.
Warning
Not all older SDK examples have been converted to the style described in this guide yet. This guide is the current norm for all new and updated examples; when an older example differs from it, follow this guide.
Conventions¶
Line Length and Indentation¶
The Cinema 4D Python SDK follows the PEP 8 guidelines but relaxes their line length limit from 79 characters to 100 characters, as this is more practical for modern documentation needs. SDK examples use four spaces per indentation level and no tabs. For users, we recommend an even more relaxed limit of 120 characters, as this is more practical when writing code on modern wide screens where printing the code on paper is not a concern (one of the original reasons for the 79 characters limit). We do however recommend adhering to a finite line length, as code interoperability is still important. Just because your code looks good on your screen with lines of 200 characters and more, it does not mean it will look good on your colleague’s smaller mobile device screen or when published on GitHub. A fixed length ensures that your code looks good everywhere. Break expressions at a natural boundary rather than shrinking names or hiding complex logic on one line.
Naming¶
Use the Cinema API’s published names as written: c4d.BaseObject, c4d.EventAdd(), and GetName(). In SDK
examples, classes, functions, and methods start with a capital letter, while variables and attributes start with a
lowercase letter and use camelCase. Constants use uppercase letters and underscores. The conventional main() entry
point is an exception. When you change an older example only slightly, you may keep its existing naming style; use the
conventions of this guide when you revise the whole example.
import c4d
def RenameSelectedObject(activeObject: c4d.BaseObject, newName: str) -> None:
"""Renames #activeObject to #newName.
"""
activeObject.SetName(newName)
Type Hinting¶
Type hinting is a feature of Python that allows authors to specify the expected types of variables, function
parameters, and return values. The Cinema 4D Python SDK uses the most recent type hinting features available in the
Python version shipped with the respective Cinema 4D release (i.e.,
PEP 484, PEP 585, and
PEP 604). We use the built-in generic and union syntax rather than the older
typing.List and typing.Optional spellings.
Type hinting is entirely optional, and code without type hints is functionally equivalent to the same code with type hints. The major purpose of type hinting is to convey the intent of the code author to users and tools (e.g., linters, IDEs, type checkers). It can help catch certain types of errors early, improve code readability, and enhance the development experience with better autocompletion and type checking in editors. E.g., the following two code blocks are functionally equivalent, but the second code block uses type hints to specify the expected types of variables and function parameters:
# Example without type hints
import myLib
def Uppercase(item):
...
data = myLib.GetData()
print(Uppercase(data))
# Example with type hints
import myLib
def Uppercase(item: str | list[str]) -> str | list[str]:
...
data: list[str] = myLib.GetData()
print(Uppercase(data))
With its type hints, the second code block hints at the fact that the function Uppercase can take either a single
string or a list of strings as input, and will return either a single uppercase string or a list of uppercase strings.
The variable data is also explicitly declared to be a list of strings. Note that type hints are by default not
checked at runtime, which for once means that they do not impact performance or behavior of the code but also that they
can be incorrect or misleading. Uppercase could for example also hint at the fact that it takes an integer as input,
and the code would still run without errors, even though actually passing an integer would result in a runtime error.
Annotating a result as c4d.BaseObject can therefore not make an API call succeed. Check optional results explicitly
or use mxutils.CheckType() where a failed call should stop the script.
But typing information can help users understand how to use the function correctly, and can also help tools catch potential type-related errors. The following table summarizes the most important syntax elements used in type hinting in the Cinema 4D Python SDK. See the Python Type Annotations documentation for more information about type hinting syntax and conventions.
Syntax |
Description |
|---|---|
|
Class names are used to denote types. E.g., import c4d
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
# Here we declare that #name is a string, #age is an integer, #item is of type Person, and
# #cube is a Cinema 4D object.
name: str = "Bob"
age: int = 30
item: Person = Person(name, age)
cube: c4d.BaseObject = c4d.BaseObject(c4d.Ocube)
|
|
The colon is used to denote the start of a type hint for variables and function arguments. In rare cases,
type hints can also be used without a variable definition, e.g., # Here we declare that #data is a list of strings and define its value at the same time.
data: list[str] = ["Alice", "Bob", "Charlie"]
# Here we just declare that #name will be a string, but do not yet define its value.
name: str
# This loop will then actually define the variable. In this case it is a bit pointless as
# both a human and linter would know by the declaration of #data of what type #name is.
for name in data:
print(f"Hello, {name}!")
# But in cases where we loop over function return values of some opaquely typed function,
# this can be useful.
obj: c4d.BaseObject
for obj in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE):
print(f"Hello, {obj.GetName()}!")
|
|
The arrow is used to denote the return type of a function. For functions that do not return a value, i.e.,
where the return type is # Here we declare that the function #Greet takes a string as argument and returns a string.
def Greet(name: str) -> str:
return f"Hello, {name}!"
message: str = Greet("Alice")
|
|
The pipe symbol is used to denote a union of types, that a variable or argument can be of multiple types.
We do not use the older # Data can be either a single string or a list of strings.
data: str | list[str] = "Hello"
# This function can take either a single string or a list of strings as its argument and
# will return either a single uppercase string or a list of uppercase strings.
def Uppercase(item: str | list[str]) -> str | list[str]:
if isinstance(item, str):
return item.upper()
elif isinstance(item, list):
return [s.upper() for s in item]
else:
raise ValueError("Invalid input type")
# An object that may be absent, as the result of a lookup.
found: c4d.BaseObject | None = doc.SearchObject("Cube")
|
|
Square brackets are used to denote collection types, i.e., a type that contains multiple elements of another type. # #names is a list of strings.
names: list[str] = ["Alice", "Bob", "Charlie"]
# #employees is a list of strings or Person objects.
employees: list[str | Person] = ["Alice", Person("Bob", 30)]
# #ages is a dictionary that maps strings to integers.
ages: dict[str, int] = {"Alice": 30, "Bob": 25, "Charlie": 35}
# #person is a tuple of exactly two elements, a string and an integer.
person: tuple[str, int] = ("Alice", 30)
# Tuples can also be of variable length: #data is a tuple of the pattern
# (float, int, float, int, ...), of arbitrary length.
data: tuple[float | int, ...] = (1.0, 2, 3.5, 4, 5.0)
# #objects is a list of Cinema 4D objects.
objects: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE)
|
|
import typing
# Here we declare that #value can be of any type. typing.Any should be used sparingly, as it
# defeats the purpose of type hinting. Usually there is a more specific annotation that can be used.
def PrintValue(value: typing.Any) -> None:
print(value)
|
|
import typing
# Here we declare that #CountUpTo is an iterator that yields integers. #Iterator effectively hints
# at the fact that a function not only returns but also yields values.
def CountUpTo(n: int) -> typing.Iterator[int]:
count: int = 1
while count <= n:
yield count
count += 1
for number in CountUpTo(5):
print(number)
|
|
import typing
# Here we declare that #Compute is a function which takes a function #f as its first argument,
# and two integers as its second and third argument. #f must have the signature [int, int], int,
# i.e., must take two integers as arguments and return an integer.
def Compute(f: typing.Callable[[int, int], int], a: int, b: int) -> int:
return f(a, b)
# Here we define a function #Add which matches the signature of #f required by #Compute.
def Add(x: int, y: int) -> int:
return x + y
# And here we call #Compute.
result: int = Compute(Add, 5, 10)
|
Script Manager supplies doc as a c4d.documents.BaseDocument and op as c4d.BaseObject | None. Declare
these hints only when you use the variables; neither name is supplied automatically in plugin modules. The following
Script Manager example handles an absent selection before accessing the object:
import c4d
doc: c4d.documents.BaseDocument # The active document.
op: c4d.BaseObject | None # The active object, can be None.
def main() -> None:
"""Renames the selected object.
"""
if op is None:
return
doc.AddUndo(c4d.UNDOTYPE_CHANGE, op)
op.SetName("Selected Object")
c4d.EventAdd()
if __name__ == "__main__":
main()
See API Overview for the three API components and Quickstart for a first runnable example.
Comprehensions¶
List comprehensions are a short-hand syntax for assembling collections without a traditionally written loop. They can make code more readable and shorter but can also be confusing to beginners, especially when nested or combined with other complex expressions. The Cinema 4D Python SDK uses list and other comprehensions quite frequently, but strives for limiting their usage to simple cases. The following code block with a loop:
# Build a list of powers of two from 0 to 9.
result: list[int] = []
for n in range(10):
result.append(2 ** n)
Can be rewritten with a list comprehension and with that reduces the code from 4 lines to a single line:
# Build a list of powers of two from 0 to 9.
result: list[int] = [2 ** n for n in range(10)]
# Comprehensions also work with other collection types, e.g., dictionaries. Here we build a dictionary
# that maps the numbers from 0 to 9 to their squares.
squares: dict[int, int] = {n: n ** 2 for n in range(10)}
Comprehensions can be used for lists, sets, tuples, dictionaries. See the Python Comprehensions documentation for more information about the subject.
A comprehension is useful for a short transformation, such as collecting the names of the selected objects. A loop is clearer when each item needs several API calls or an error check, or when the data is not a plain collection, such as when walking an object hierarchy. Cinema 4D hierarchies are traversed through the first child and the next sibling:
# Gather the names of the selected objects with a comprehension.
selected: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE)
selectedNames: list[str] = [obj.GetName() for obj in selected]
# Gather the names of the children of #op with a loop. Use a loop like this one if you later need to
# inspect or modify each child.
childNames: list[str] = []
child: c4d.BaseObject | None = op.GetDown()
while child is not None:
childNames.append(child.GetName())
child = child.GetNext()
Example Structure¶
Start a runnable SDK example with a short module docstring describing its purpose and how to run it. Put its work in
main() and invoke it in an if __name__ == "__main__": block. Use concise Google-style docstrings for functions,
ending the summary with a line break, and refer to arguments with a leading #, e.g., #newName. Include the
author, license, copyright, and version fields specified in py_sdk/AGENTS.md for new SDK examples. Comments should
explain decisions rather than repeat the next API call, and are clustered rather than placed above every line.
Best Practices¶
Cinema 4D is a shared Python environment, meaning that all scripts and plugins running in Cinema 4D share the same Python interpreter and the same global state. This has some implications for writing code that collides with what one might be used to from volatile Python environments, such as scripts running in a standard CPython VM.
Importing Libraries¶
Another cause for issues can be colliding dependencies in a shared environment. In a standard CPython VM, the path of
the executed script is automatically added to the module search paths (i.e., sys.path) when the script is executed.
This is not the case for a shared Python VM such as Cinema 4D, as adding all found scripts and plugin folders to the
module search paths would lead to unpredictable importing behavior. This means that you must expose the script
directory yourself if you want to import modules from the same directory as your script. Use
mxutils.LocalImportPath for this, which exposes a directory only for the duration of a with block. See
Python Environment and External Libraries for more things to consider due to the shared
nature of the Cinema 4D Python VM.
import os
import sys
# This is a really bad idea, as it will not only expose your search paths to all other scripts and plugins
# running in Cinema 4D, but can also cause module name collisions.
libs: str = os.path.join(os.path.dirname(__file__), "libs")
if libs not in sys.path:
sys.path.insert(0, libs)
# The local math library collides with the builtin module of the same name.
from libs import math
import mxutils
# Do this instead: the directory of this file is only part of the search paths within the block.
with mxutils.LocalImportPath(__file__):
import myLocalModule
StdIn/StdOut/StdErr¶
Avoid overwriting sys.stdin, sys.stdout, or sys.stderr. The Cinema 4D Python VM uses a custom stream handler
to redirect standard output and error streams to the Cinema 4D Python console. This means that you cannot easily
overwrite the stream handlers, as this will break the output redirection to the console.
import sys
# This will break the output redirection to the Cinema 4D console. There is no way to reestablish the
# redirection with a custom stream handler.
sys.stdout = open("output.txt", "w")
print("This will not appear in the Cinema 4D console.")
Python Processes¶
Do not attempt to spawn new Python processes (e.g., via multiprocessing or subprocess), and do not use Python’s
threading to operate on Cinema 4D data. The Cinema 4D Python VM is the Cinema 4D executable itself, and neither does
it understand Python-specific command line arguments, nor will it be able to communicate with other running Cinema 4D
instances. Use c4d.threading for threading and the standalone interpreter, see
Standalone Interpreter, when you need a separate Python process.
import multiprocessing
import runpy
import subprocess
import sys
# This will not work as expected, as sys.executable points to the Cinema 4D executable.
subprocess.run([sys.executable, "myScript.py"], check=True)
# This is a valid alternative to subprocess, but it will run in the same Python process and therefore
# will not have the performance and isolation benefits of a separate process.
runpy.run_path("myScript.py", run_name="__main__")
# multiprocessing is not supported at all for the same reason, as multiprocessing just spawns new
# Python processes and then handles their inter-process communication. There is no alternative or
# workaround for this, and trying it can crash Cinema 4D.
def Worker():
print("Worker process")
pool: list[multiprocessing.Process] = [
multiprocessing.Process(target=Worker) for _ in range(4)
]
Working Directory¶
The working directory of the Cinema 4D Python VM is not the directory of your script. This differs from a common CPython instance, where scripts are often run from their own directory. This becomes especially relevant when using relative paths in your scripts. It is also not recommended to change the current working directory of the Cinema 4D Python VM, as this violates the rule of not modifying the state of shared builtin module objects. Other plugins and scripts might rely on the working directory not changing. When working with files, always use absolute paths or paths relative to the script directory. The following code block shows a bad and a good example:
import os
# This will try to open the file "myFile.txt" relative to the working directory of Cinema 4D, not in
# the directory of this script.
with open("myFile.txt", "r") as file:
data = file.read()
# Do this instead.
scriptDirectory: str = os.path.dirname(__file__)
with open(os.path.join(scriptDirectory, "myFile.txt"), "r") as file:
data = file.read()
Document Changes¶
Script Manager provides doc and op; plugins and scene scripting elements do not receive those same variables
automatically. A Script Manager command that edits the document should register supported undo steps and call
c4d.EventAdd() once after its edits. An execution callback, such as a Python Generator’s main(), must not
modify the active document or show interface dialogs. See Scripting Elements and
Scene Execution for the restrictions on execution and threads.
Plugins¶
Obtain a unique plugin ID from the developer portal; never ship an SDK example ID in a plugin. Keep .pyp code,
resources, descriptions, and icons together as described in Implementing Plugins.