#set document(title: "10.3 Calling Mojo from Python", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 10.3#h(0.6em)Calling Mojo from Python If you have an existing Python project that would benefit from Mojo's high-performance computing, you shouldn't have to rewrite the whole thing in Mojo. Instead, you can write just the performance-critical parts your code in Mojo and then call it from Python. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Beta feature] Calling Mojo code from Python is in early development. You should expect a lot of changes to the API and ergonomics. Likewise, this documentation is still a work in progress. See below for known limitations. ] === Import a Mojo module in Python To illustrate what calling Mojo from Python looks like, we'll start with a simple example, and then dig into the details of how it works and what is possible today. Consider a project with the following structure: project ├── 🐍 main.py └── 🔥 mojo\_module.mojo The main entrypoint is a Python program called main.py, and the Mojo code includes functions to call from Python. For example, let's say we want a Mojo function to take a Python value as an argument: def factorial(py\_obj: PythonObject) raises -\> Python var n = Int(py=py\_obj) return math.factorial(n) And we want to call it from Python like this: import mojo\_module print(mojo\_module.factorial(5)) However, before we can call the Mojo function from Python, we must declare it so Python knows it exists. Because Python is trying to load mojo\_module, it looks for a function called PyInit\_mojo\_module(). (If our file was called foo.mojo, the function Python looked for would be PyInit\_foo().) Within the PyInit\_mojo\_module(), we must declare all Mojo functions and types that are callable from Python using #link("https://mojolang.org/docs/std/python/bindings/PythonModuleBuilder/")[PythonModuleBuilder]. So the complete Mojo code looks like this: from std.python import PythonObject from std.python.bindings import PythonModuleBuilder from std import math from std.os import abort \@export def PyInit\_mojo\_module() abi("C") -\> PythonObject: try: var m = PythonModuleBuilder("mojo\_module") m.def\_function\[factorial\]("factorial", docstring="Compute n!") return m.finalize() except e: abort(String("error creating Python Mojo module:", e)) def factorial(py\_obj: PythonObject) raises -\> PythonObject: \# Raises an exception if \`py\_obj\` is not convertible to a Mojo \`Int\`. var n = Int(py=py\_obj) return math.factorial(n) On the Python side, we add the directory containing mojo\_module.mojo to the Python path, and then use a normal import statement to load our Mojo code: import mojo.importer import mojo\_module print(mojo\_module.factorial(5)) That's it! Try it: python main.py 120 ==== How it works Python supports a standard mechanism called #link("https://docs.python.org/3/extending/extending.html")[Python extension modules] that enables compiled languages (like Mojo, C, C++, or Rust) to make themselves callable from Python in an intuitive way. Concretely, a Python extension module is simply a dynamic library that defines a suitable PyInit\_\* function. Mojo comes with built-in functionality for defining Python extension modules. The special stuff happens in the mojo.importer module we imported. If we have a look at the filesystem after Python imports the Mojo code, we'll notice there's a new \_\_mojocache\_\_ directory, with a dynamic library (.so) file inside: project ├── main.py ├── mojo\_module.mojo └── \_\_mojocache\_\_ └── mojo\_module.hash-ABC123.so Loading mojo.importer loads our Python Mojo #link("https://docs.python.org/3/reference/import.html#import-hooks")[import hook], which behind the scenes looks for a .mojo file that matches the imported module name, and if found, compiles it using #link("https://mojolang.org/docs/cli/build/#--emit-file_type")[mojo build --emit shared-lib] to generate a dynamic library. The resulting file is stored in \_\_mojocache\_\_, and is rebuilt only when it becomes stale (typically, when the Mojo source file changes). #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Clearing cached build artifacts] The \_\_mojocache\_\_ directory should contain only derived artifacts. It is always safe to delete the contents of a \_\_mojocache\_\_ directory. Needed artifacts will simply be rebuilt the next time the Mojo module is imported. ] ==== The abi of exported functions An #link("https://mojolang.org/docs/reference/decorators/export/")[\@export] function must declare which calling convention it uses with an explicit #link("https://mojolang.org/docs/reference/function-declarations#abi-c")[abi] effect. In a Python extension module, the only function you need to export is the PyInit\_\ entry point, and it must use abi("C"): \@export def PyInit\_mojo\_module() abi("C") -\> PythonObject: ... This is because the CPython runtime locates and calls PyInit\_\ directly across the C boundary, so it must expose the C calling convention. A abi("C") function can't be marked raises, which is why the examples above catch any error inside the body and abort instead of propagating it. The functions, methods, and initializers you register with the module builder (def\_function, def\_method, def\_py\_init, and so on) don't need \@export at all; you pass them by reference, and Mojo generates the C wrapper that CPython actually calls. That wrapper invokes your function using the Mojo calling convention and translates any raised error into a Python exception, so a registered function such as factorial above can freely be marked raises. Now that we've looked at the basics of how Mojo can be used from Python, let's dig into the available features and how you can leverage them to accelerate your Python with Mojo. === Bindings features ==== Binding Mojo types You can bind any Mojo type for use in Python using #link("https://mojolang.org/docs/std/python/bindings/PythonModuleBuilder/")[PythonModuleBuilder]. For example: \@fieldwise\_init struct Person(Movable, Writable): var name: String var age: Int \@export def PyInit\_person\_module() abi("C") -\> PythonObject: try: var mb = PythonModuleBuilder("person\_module") var person\_type = mb.add\_type\[Person\]("Person") except e: abort("error creating Mojo module") When you call #link("https://mojolang.org/docs/std/python/bindings/PythonModuleBuilder/#add_type")[add\_type()], it returns a #link("https://mojolang.org/docs/std/python/bindings/PythonTypeBuilder/")[PythonTypeBuilder], which you can then use to bind the type constructor (see binding Python initializers, below) and methods. Any Mojo type bound using a PythonTypeBuilder has the resulting Python 'type' object globally registered, enabling two features: - Constructing Python objects that wrap Mojo values for use from Python using PythonObject(alloc=Person(..)). - Downcasting using python\_obj.downcast\_value\_ptr\[Person\]() #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Mojo types must implement #link("https://mojolang.org/docs/std/format/Writable/")[Writable] to be bound for use in Python. Additional traits are required for specific binding features: Movable for custom initializers (def\_py\_init), and both Defaultable and Movable for default initializers (def\_init\_defaultable). ] However, merely binding a Mojo type to a Python type object isn't very useful on its own. Next, we'll tell Python how to interact with our Mojo type—starting with how to construct instances of our Mojo type from within Python. ==== Constructing Mojo objects in Python Mojo types can be constructed from Python by declaring a Mojo constructor function as a Python-compatible object initializer using #link("https://mojolang.org/docs/std/python/bindings/PythonTypeBuilder/#def_py_init")[def\_py\_init()] when you add the type to your module. For example: \@export def PyInit\_person\_module() abi("C") -\> PythonObject: try: var mb = PythonModuleBuilder("person\_module") \# highlight-start \_ = mb.add\_type\[Person\]("Person").def\_py\_init\[Person.py\_init\]() \# highlight-end return mb.finalize() except e: abort(String("error creating Python Mojo module:", e)) \@fieldwise\_init struct Person(Movable, Writable): var name: String var age: Int \# highlight-start \@staticmethod def py\_init( out self: Person, args: PythonObject, kwargs: PythonObject ) raises: \# Validate argument count if len(args) != 2: raise Error("Person() takes exactly 2 arguments") \# Convert Python arguments to Mojo types var name = String(args\[0\]) var age = Int(args\[1\]) self = Self(name, age) \# highlight-end With this Mojo binding, you can create Person instances in Python: person = person\_module.Person("Sarah", 32) print(person) Person(name=Sarah, age=32) For types that support default construction, you can use the simpler #link("https://mojolang.org/docs/std/python/bindings/PythonTypeBuilder/#def_init_defaultable")[def\_init\_defaultable()] method: var counter\_type = m.add\_type\[Counter\]("Counter") counter\_type.def\_init\_defaultable\[Counter\]() This enables Python code to create instances without arguments: counter = counter\_module.Counter() \# Creates Counter() #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph["Constructor" vs "Initializer"] In Python, object construction happens across both the \_\_new\_\_() and \_\_init\_\_() methods, so the \_\_init\_\_() method is technically just the attribute initializer. However, in a Mojo struct, there's no \_\_new\_\_() method, so we prefer to always call \_\_init\_\_() the constructor. ] ==== Returning Mojo objects to Python Mojo functions called from Python don't just need to be able to accept #link("https://mojolang.org/docs/std/python/python_object/PythonObject/")[PythonObject] values as arguments, they also need to be able to return new values. And sometimes, they even need to be able to return Mojo native values back to Python. This is possible by using the PythonObject(alloc=\) constructor. An example of this looks like: def create\_person() -\> PythonObject: var person = Person("Sarah", 32) return PythonObject(alloc=person^) #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ PythonObject(alloc=...) will raise an exception if the provided Mojo object type had not previously been registered using #link("https://mojolang.org/docs/std/python/bindings/PythonModuleBuilder/#add_type")[PythonModuleBuilder.add\_type()]. ] ==== PythonObject to Mojo values Within any Mojo code that is handling a #link("https://mojolang.org/docs/std/python/python_object/PythonObject/")[PythonObject], but especially within Mojo functions called from Python, it's common to expect an argument of a particular type. There are two ways in which a PythonObject can be turned into a native Mojo value: - #strong[Converting] a Python object into a newly constructed Mojo value that has the same logical value as the original Python object. This is handled by the \[ConvertibleFromPython\]\[ConvertibleFromPython\] trait. - #strong[Downcasting] a Python object that holds a native Mojo value to a pointer to that inner value. This is handled by \[PythonObject.downcast\_value\_ptr()\]\[downcast\_value\_ptr\]. PythonObject conversions Many Mojo types support conversion directly from equivalent Python types, via the \[ConvertibleFromPython\]\[ConvertibleFromPython\] trait: \# Given a person, clone them and give them a different name. def create\_person( name\_obj: PythonObject, age\_obj: PythonObject ) raises -\> PythonObject: \# These conversions will raise an exception if they fail var name = String(name\_obj) var age = Int(age\_obj) return PythonObject(alloc=Person(name, age)) Which could be called from Python using: person = mojo\_module.create\_person("John Smith") Passing invalid arguments will result in a runtime argument error: person = mojo\_module.create\_person(42) PythonObject downcasts Downcasting from PythonObject values to the inner Mojo value: def print\_age(person\_obj: PythonObject) raises: \# Raises if \`obj\` does not contain an instance of the Mojo \`Person\` type. var person = person\_obj.downcast\_value\_ptr\[Person\]() print("Person is", person\[\].age, "years old") Unsafe mutation via downcasting is also supported. It is up to the user to ensure that this mutable pointer does not alias any other pointers to the same object within Mojo: def birthday(person\_obj: PythonObject): var person = person\_obj.downcast\_value\_ptr\[Person\]() person\[\].age += 1 Entirely unchecked downcasting—which does no type checking—can be done using: def get\_person(person\_obj: PythonObject): var person = person\_obj.unchecked\_downcast\_value\_ptr\[Person\]() Unchecked downcasting can be used to eliminate overhead when optimizing a tight inner loop with Mojo, and you've benchmarked and measured that type checking downcasts is a significant bottleneck. ==== Methods When binding Mojo objects for use from Python, you can expose chosen methods to Python as well, using #link("https://mojolang.org/docs/std/python/bindings/PythonTypeBuilder/#def_method")[PythonTypeBuilder.def\_method()]. Currently, Mojo methods being exposed to Python must be written with a modification compared to normal Mojo methods: they must be a \@staticmethod that takes either py\_self: PythonObject or self\_ptr: UnsafePointer\[Self\]: from std.python import PythonObject from std.python.bindings import PythonModuleBuilder from std.os import abort \@export def PyInit\_mojo\_module() abi("C") -\> PythonObject: try: var mb = PythonModuleBuilder("mojo\_module") \# highlight-start \_ = mb.add\_type\[Person\]("Person") .def\_method\[Person.get\_name\]("get\_name") .def\_method\[Person.set\_age\]("set\_age") \# highlight-end return mb.finalize() except e: abort("error creating Mojo module") struct Person(Writable): var name: String var age: Int \# highlight-start \@staticmethod def get\_name(py\_self: PythonObject) raises -\> PythonObject: var self\_ptr = py\_self.downcast\_value\_ptr\[Self\]() return self\_ptr\[\].name \@staticmethod def set\_age( self\_ptr: UnsafePointer\[mut=True, Self\], new\_age: PythonObject, ) raises: self\_ptr\[\].age = Int(new\_age) \# highlight-end def write\_to(self, mut writer: Some\[Writer\]): t"Person({self.name}, {self.age})".write\_to(writer) Taking py\_self: PythonObject allows access to the full PythonObject allocation that a Mojo object instance is stored inside of. Typically though, taking py\_self: UnsafePointer\[Self\] will minimize boilerplate in the common case that a method merely needs to access the fields of an object. Mojo methods called from Python are currently required to take non-standard self types due to limitations that will be lifted in future versions of Python Mojo bindings. ==== Static methods Python Mojo bindings supports exposing Python \@staticmethods, bound using #link("https://mojolang.org/docs/std/python/bindings/PythonTypeBuilder/#def_staticmethod")[PythonTypeBuilder.def\_staticmethod()]. A function declared using def\_staticmethod() is callable as a static method on the type within Python, without needing an object instance. from std.python import PythonObject from std.python.bindings import PythonModuleBuilder from std.os import abort \@export def PyInit\_mojo\_module() abi("C") -\> PythonObject: try: var mb = PythonModuleBuilder("mojo\_module") \# highlight-start mb.add\_type\[Person\]("Person") .def\_staticmethod\[Person.is\_valid\_age\]("is\_valid\_age") \# highlight-end return mb.finalize() except e: abort("error creating Mojo module") struct Person(Writable): var name: String var age: Int \# highlight-start \@staticmethod def is\_valid\_age(age\_obj: PythonObject) raises -\> PythonObject: var age = Int(age\_obj) return 0 \<= age \<= 130 \# highlight-end def write\_to(self, mut writer: Some\[Writer\]): t"Person({self.name}, {self.age})".write\_to(writer) Calling a Mojo function bound as a static method looks like a typical Python static method call directly on the type object: from mojo\_module import Person print(Person.is\_valid\_age(45)) \# Prints 'True' print(Person.is\_valid\_age(-1)) \# Prints 'False' ==== Keyword arguments Keyword arguments in Mojo come in two forms: + Keyword-only arguments: def foo(\*, x: Int) This is not currently supported in Python Mojo bindings. + Variadic keyword arguments: def foo(\*\*kwargs: Int) This is supported in Python Mojo bindings when used in the unsugared form: def foo(kwargs: OwnedKwargsDict). (The \*\*kwargs syntax limitation will be removed in the future.) You can define Mojo functions that accept variadic keyword arguments using #link("https://mojolang.org/docs/std/collections/dict/OwnedKwargsDict/")[OwnedKwargsDict\[PythonObject\]] as the last argument. A simple example looks like: import mojo\_module result = mojo\_module.sum\_kwargs\_ints(a=10, b=20, c=30) \# returns 60 from std.collections import OwnedKwargsDict def sum\_kwargs\_ints(kwargs: OwnedKwargsDict\[PythonObject\]) raises -\> PythonObject: var total = 0 for entry in kwargs.items(): total += Int(entry.value) return PythonObject(total) Keyword arguments are also supported following normal positional arguments. Additionally, getting specific keyword arguments is a dictionary lookup on the OwnedKwargsDict: from std.collections import OwnedKwargsDict def duration\_in\_seconds( hours\_obj: PythonObject, minutes\_obj: PythonObject, kwargs: OwnedKwargsDict\[PythonObject\] ) raises -\> PythonObject: var hours = Int(hours\_obj) var minutes = Int(minutes\_obj) var seconds = Int(kwargs\["seconds"\]) return hours \* 3600 + minutes \* 60 + seconds In this example, if a call to duration\_in\_seconds() is missing the required "seconds" named argument, a runtime exception will occur: from mojo\_module import duration\_in\_seconds \# Pass hours and minutes, missing "seconds" duration\_in\_seconds(4, 5) \# ERROR: KeyError Keyword arguments are supported when bindings top-level functions, methods, and static methods. ==== Variadic arguments Python and Mojo variadic arguments are normally written using the following syntax: def foo(\*args: Int): ... However, this syntax is not yet supported in Python/Mojo bindings, because functions bound using #link("https://mojolang.org/docs/std/python/bindings/PythonModuleBuilder/#def_function")[def\_function()] support only fixed-arity functions. As a workaround, you can expose Mojo functions that accept a variadic number of arguments to Python using the lower-level #link("https://mojolang.org/docs/std/python/bindings/PythonModuleBuilder/#def_py_function")[def\_py\_function()] interface, which leaves it to the user to validate the number of arguments provided: \@export def PyInit\_mojo\_module() abi("C") -\> PythonObject: try: var b = PythonModuleBuilder("mojo\_module") b.def\_py\_function\[count\_args\]("count\_args") b.def\_py\_function\[sum\_args\]("sum\_args") b.def\_py\_function\[lookup\]("lookup") def count\_args(py\_self: PythonObject, args\_tuple: PythonObject) raises: return len(args\_tuple) def sum\_args(py\_self: PythonObject, args\_tuple: PythonObject) raises: var total = args\_tuple\[0\] for i in range(1, len(args\_tuple)): total += args\_tuple\[i\] return total def lookup(py\_self: PythonObject, args\_tuple: PythonObject) raises: if len(args\_tuple) != 2 and len(args\_tuple) != 3: raise Error("lookup() expects 2 or 3 arguments") var collection = args\_tuple\[0\] var key = args\_tuple\[1\] try: return collection\[key\] except e: if len(args) == 3: return args\_tuple\[2\] else: raise e === Strategies for porting Python to Mojo ==== Writing Pythonic code in Mojo In this approach to bindings, we embrace the flexibility of Python, and eschew trying to convert PythonObject arguments into the narrowly constrained, strongly-typed space of the Mojo type system, in favor of just writing some code and letting it raise an exception at runtime if we got something wrong. The flexibility of PythonObject enables a unique programming style, wherein Python code can be "ported" to Mojo with relatively few changes. def foo(x, y, z): x\[y\] = int(z) x = y + z Rule of thumb: Any Python builtin function should be accessible in Mojo using Python.\. def foo(x: PythonObject, y: PythonObject, z: PythonObject) -\> PythonObject: x\[y\] = Python.int(z) x = y + z === Building Mojo extension modules You can create and distribute your Mojo modules for Python in the following ways: - As source files, compiled on demand using the Python Mojo importer hook. The advantage of this approach is that it's easy to get started with, and keeps your project structure simple, while ensuring that your imported Mojo code is always up to date after you make an edit. - As pre-built Python extension module .so dynamic libraries, compiled using: mojo build mojo\_module.mojo --emit shared-lib -o mojo\_module.so This has the advantage that you can specify any other necessary build options manually (optimization or debug flags, import paths, etc.), providing an "escape hatch" from the Mojo import hook abstraction for advanced users. === Known limitations While we have big ambitions for Python to Mojo interoperability—our goal is for Mojo to be the best way to extend Python—this feature is still in early and active development, and there are some limitations to be aware of. These will be lifted over time. - #strong[Functions taking more than 6 arguments.] Currently PyTypeBuilder.add\_function() and related function bindings only support Mojo functions that take up to 6 PythonObject arguments: def(PythonObject, PythonObject, PythonObject, PythonObject, PythonObject, PythonObject). - #strong[Keyword arguments syntax.] Currently, Mojo functions called from Python only accept keyword arguments when using a trailing kwargs: OwnedKwargsDict\[PythonObject\] argument. Support for native \*\*kwargs syntax will be added in the future. - #strong[Mojo package dependencies.] Mojo code that has dependencies on packages other than the Mojo stdlib (like those in the ever-growing #link("https://github.com/modular/modular-community")[Modular Community] package channel) are currently only supported when building Mojo extension modules manually, as the Mojo import hook does not currently support a way to specify import paths for Mojo package dependencies. - #strong[Properties.] Computed properties getter and setters are not currently supported. - #strong[Expected type conversions.] A handful of Mojo standard library types can be constructed directly from equivalent Python builtin object types, by implementing the \[ConvertibleFromPython\]\[ConvertibleFromPython\] trait. However, many Mojo standard library types do not yet implement this trait, so may require manual conversion logic if needed. \[ConvertibleFromPython\]: /docs/std/python/conversions/ConvertibleFromPython/ \[downcast\_value\_ptr\]: /docs/std/python/python\_object/PythonObject\#downcast\_value\_ptr