What is the meaning of code like `doc: c4d.documents.BaseDocument`?
-
Hey Ferdinand!
Thank you for the clarification.
Forgive my ignorance, I am new to Python, give me a link to the material describing the notation...doc: c4d.documents.BaseDocument # The currently active document. op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None` def main() -> None:
I understand that the literals "doc:", "op:" denote variables, but variables are assigned using the "=" sign, and here the ":" sign is used.
What is the difference?"def main() -> None:" as I understand it, indicates that the main() function does not return anything.
Could you send me a link where I can read about this notation?
edit (@ferdinand): I have forked this question since it deviated from the subject of the original posting. Please open new topics for new subjects. But since this question comes up again and again, I decided to answer it in an exhaustive manner for future reference. This subject is technically out of scope of support, as it is about a Python feature and we cannot teach you how Python works (but I made here an exception as this keeps confusing beginners).
-
Hello @lednevandrey,
the question you ask is a common question for beginners, and there is no shame in asking it. The syntax can look a bit alien (even when one is accustomed to things like TypeScript).
Type hinting overview
The practice you mention, annotating attributes, variables, arguments, and return values with meta information, is called type hinting. This feature has been introduced with Python 3.5 and is very much still in active development as of Python 3.13, Python 3.13 for example brings new type hinting features and there are other features in the pipeline for future versions. Type hinting is somewhat similar in as to what TypeScript does for JavaScript. But other than for TypeScript, type hinting is entirely cosmetic unless one is using libraries like Pydantic. This means one can remove all type hinting and this changes nothing about the runtime behavior of a script.
At Maxon we use type hinting to write public Python code that is easier to understand for beginners, as it is more verbose about what is what. I personally also use type hinting in production code I write, but if type hinting is good or bad is somewhat a religious question in the Python community.
To clarify what is happening with type hinting, we can look at this Python code:
f: float = 3.14 i: int = 42 def foo(name: str, upperCase: bool = True) -> list[str]: return [n.upper() if upperCase else n for n in name] x: bool = "Hello world!"
This code above is using type hinting. It is functionally identical to the code shown below:
f = 3.14 i = 42 def foo(name, upperCase = True): return [n.upper() if upperCase else n for n in name] x = "Hello world!"
The first code section declares that
f
andi
are of typefloat
andint
. And that there is a functionfoo
which takes astr
and abool
as arguments and returns alist
ofstr
as its return value. What we can also see, is that it incorrectly declares thatx
is of typebool
although it is astr
. Python does not care that the type hint forx
is wrong and will happily run this for us (unless we use something likePydantic
).Type hinting is (usually) intellectually generated meta information with absolutely no guarantee that it is correct and no impact on runtime behavior.
Type hinting fulfills four main purposes:
- It makes code more readable regarding the type nature of entities.
data: list[str] = foo("Bob")
is much more verbose thandata = foo("Bob")
. - It allows meta-tools like linters or auto-complete to work. When you define a function
foo(a)
, these tools have no clue whata
is, and when you then typea.
(implying thata
has attributes likea.data
ora.run()
you want to type out) or do something witha
you should not, they will not be able to help you. But when you writefoo(a: c4d.BaseObject)
, these tools will be able to help you. - As discussed below, type hinting can also have a purely declarative purpose, helping to expose data which is injected from the outside.
- And finally, type hinting is a perfect excuse for the Python community to have some drama over what is Pythonic and what is not. It is the noble "Python is a type free language" purists against the filthy "static typing through the backdoor introducing" heretics. Nerd wars!!!
Type hinting in its purely declarative syntax
Type hinting can also be used in a purely declarative manner, i.e., in a manner that just declares that something of a certain type will exist. One example for this is unpacking, currently there is not syntax for type hinting unpacked values. E.g., for this:
data: list[tuple[str, int, float]] = [("Alice", 26, 172.6), ("Bob", 36, 186.2)] for name, age, height in data: ...
there is is currently no syntax to directly type hint at what
name
,age
, andheight
are. But we can use the declarative syntax to hint at ahead what these variables will be.# Here in this context it is not super obvious what #data is, maybe we got it passed in from # the outside. Before we iterate over data and unpack its values, we declare of what type these # values will be (this is a bit ugly but currently the only way to do this). name: str age: int height: float for name, age, height in data: ...
What does doc: c4d.documents.BaseDocument mean?
Something similar is happening in the context of the many script types in Cinema 4D.
import c4d # We declare that this module always will have access to two 'variables' # named `op` and `doc` without actually defining them. op: c4d.BaseObject | None doc: c4d.documents.BaseDocument
Modules are objects
This is section contains expert information which can be ignored by most users.
The background to this is that everything is an object in Python. This includes modules, which is Python slang for a code file (a little bit incorrect but close enough). When a module is being executed, the Python virtual machine, or in this case Cinema 4D, can inject arbitrary things into the to be executed module. As an approximation we can look at this script which executes scripts in
c4dpy
as if they were script manager scripts (not exactly the same as what happens in the backend, but again close enough):# We load a document. doc = c4d.documents.LoadDocument( name=in_path, loadflags=c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, thread=None) ... # We make the document the active document and get the currently selected # object and the Thinking Particles particle system of the document. c4d.documents.SetActiveDocument(doc) op = doc.GetActiveObject() tp = doc.GetParticleSystem() # Now we execute the script, a.k.a, module, with this data. data = runpy.run_path( script_path, init_globals={"doc": doc, "op": op, "tp": tp}, run_name="__main__")
First, we load a document into a variable
doc
, and then get the active object and particle system in that document asop
andtp
. Then we userunpy
to execute the file (a.k.a module) atscript_path
. In this call we do two things:- We init the globals of the module with the three variables defined earlier by passing them as a dictionary, the globals with which the module
script_path
shall be initalized. - We set the execution name to
__main__
(which is why the characteristicif __name__ == "__main__"
exists in Python scripts that are called directly).
If one wants to get a more precise understanding of what happens when the backend of Cinema 4D executes a Python script module, one can look at this (again, not 100% what happens in the backend, but much closer than the Python script example).
What are globals()?
So, what is this
init_globals
we pass torunpy
? It looks very similar to what we can find in a Cinema 4D script? Python's documentation is a bit thin lipped as to what these 'globals' are:globals(): Returns the dictionary implementing the current module namespace. For code within functions, this is set when the function is defined and remains the same regardless of where the function is called.
What the documentation means with this, is that every module has a set of attributes which represent the content of that module. Or in less fancy language: A module is just an object that has a bunch of attributes. This implies that something like a global variable, constant, or function does not actually exist in Python, as there is always a wrapping module object that owns such 'top' level entities (a global variable or function implies something that lives ungoverned by any other entity). Which is also why people sometimes talk about module attributes in Python; and then usually mean global variables or constants (but a module attribute can also be function or class declaration object). We can again use some Python to illustrate all this:
# This module is meant to be executed with a vanilla CPython, i.e., # 'normal' Python. You can also run this with Cinema 4D, but the output # will be slightly different, Cinema 4D will also inject #op and #doc # into this module. import types # A module is type as an other, we can just instantiate it. The dirty # secret is out, modules are just objects. Gasp !!!!!! module: types.ModuleType = types.ModuleType('__main__') import os print(os, module) # Prints "<module 'os' (frozen)> <module '__main__'>"" print(type(os) == type(module)) # Prints "True" # Now we add attributes to the module, this is not any different than for # any other object, e.g., a class instance. If we would do this 'naturally', # we would just define these in the scope of a module. I.e., in plain English, # would 'write' them in a .py file. module.f = 3.1415 module.i = 42 def Foo() : pass module.Foo = Foo class Bar: pass module.Bar = Bar # Now we inspect the 'globals' of the module. Here we also see that the idea of # 'globals' as something special is kind of fake. It is just as any other object, # a module holds its attributes in its __dict__ dictionary. print (module.__dict__) # This will print this, so a module is effectively just a dictionary which holds # all the stuff defined in a file, ready for usage. There are some special # attributes, Python refers to these as dunder (double underscore) attributes, but # they are largely irrelevant for us. But we can see again the execution context # __name__ which we already know. # { '__name__': '__main__', '__doc__': None, '__package__': None, # '__loader__': None, '__spec__': None, 'f': 3.1415, 'i': 32, # 'Foo': <function Foo at 0x000001D5595A1440>, # 'Bar': <class '__main__.Bar'> }
Putting everything together
We can now play the same game with a 'real' module, in this case as Cinema 4D script manager script.
# Meant to be run in Cinema 4D as a script manager script. import c4d # The pure declaration of #doc, we do not actually define what #doc is. We do not even # declare #op to demonstrate that it is irrelevant if we declare something or not. doc: c4d.documents.BaseDocument # Some module attributes, a.k.a., "globals" f: float = 3.1415 i: int = 42 def Foo(): pass class Bar: pass # With globals() we can access the attributes of the module we are currently in. It is the same # as if we would get hold of this module object (possible via `sys.modules[__name__]`) and then # get its dict. # When we print out the content, things are very similar to our prior example. The dunder # attributes hold a bit more meta information in a 'real' module, but otherwise things are # identical. Since this is a Cinema 4D Script Manager script, we can also see #doc and #op # being injected into the module. The backend does not care if we declare #op or #doc, they # are always being injected into the module before execution. Our `doc: c4d.documents...` # at the beginning is just fluff (which ends up in the __annotations__ attribute of the # module). print(globals()) # { '__name__': '__main__', # '__doc__': None, # '__package__': None, # '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001B9E6E79E00>, # '__spec__': None, # '__annotations__': {'doc': <class 'c4d.documents.BaseDocument'>, 'f': <class 'float'>, 'i': <class 'int'>}, # '__builtins__': <module 'c4d' (built-in)>, # '__file__': 'c:\\Users\\Foo\\OneDrive\\Desktop\\test.py', # '__cached__': None, # 'c4d': <module 'c4d' (built-in)>, # 'doc': <c4d.documents.BaseDocument object at 0x0000025867F19C80>, # 'op': <c4d.BaseObject object called Cube/Cube with ID 5159 at 2544283200960>, # 'f': 3.1415, # 'i': 42, # 'Foo': <function Foo at 0x000001B9E6EA1440>, # 'Bar': <class '__main__.Bar'> }
Summary
So, while type hinting might look a bit alien at first glance, it serves a relatively simple purpose:
- Type hinting is the practice in Python to declare the type of constants, variables, attributes, arguments, and function return values, e.g.,
foo: int = 42
- Type hinting is optional and mostly serves the purpose to make code less ambiguous and more readable.
- Type hinting can also be used to declare something in advance before it actually exists, e.g.,
foo: int
. doc
,op
,tp
and similar declarations found in default scripts in Cinema 4D indicate objects that are injected by the Python backend into these modules upon execution for the user's convenience.
Cheers,
Ferdinand - It makes code more readable regarding the type nature of entities.
-
Lovely tips! I'm a big fan of type hint (deeply influenced by the Ferdinand code style while studying)
Cheers~
DunHou -
Hello, Ferdinand!
Thank you for your attention to my question, your time, and your academic explanations.