Skip to main content

Static Types in ScalaPy

One of the most important parts of Scala is its strong type system. ScalaPy lets you continue using this even as you use dynamically-typed Python libraries by defining static type definitions.

Defining Type Facades

Creating type definitions in ScalaPy is very similar to creating them with Scala.js. Type definitions are just annotated traits with methods defining what is available on the underlying Python value.

For example, we could define a static type definition for the built-in string type.

import me.shadaj.scalapy.py

@py.native trait PyString extends py.Object {
def count(subsequence: String): Int = py.native
}

Note: If using Scala 3, you should define a static type as a subclass, instead of a trait as shown in this example.

Once you have this type facade, it is usable with the .as method just like converting to existing Scala types. So, to get a type-safe reference to the Python string we have loaded, we convert it to our facade type.

val string = py.module("string").digits.as[PyString]
// string: PyString = 0123456789
string.count("123")
// res0: Int = 1

If we try to call this method with the wrong parameter type, we get the expected error message

string.count(123)
// error: type mismatch;
// found : Int(123)
// required: String
// string.count(123)
// ^^^

Scala methods representing bracket access

The annotation @PyBracketAccess can be used on methods to mark them as representing indexing into the Python object using brackets in Python syntax. The target method must have one (to read the value) or two parameters (to update the value). For example, we can create a static facade for a list of integers:

import py.PyBracketAccess

@py.native trait IntList extends py.Any {
@PyBracketAccess
def apply(index: Int): Int = py.native

@PyBracketAccess
def update(index: Int, newValue: Int): Unit = py.native
}

Then let's create a Python list:

import py.PyQuote

val myList = py"[1, 2, 3]".as[IntList]
// myList: IntList = [4, 2, 3]

And now we can just use brackets to access elements by indexes. For example, we want to get element at index 0:

myList(0) // in Python it will call `myList[0]` and return 1
// res2: Int = 1

We can also update elements of the list in the following way:

myList(0) = 4 // the updated list will be: [4, 2, 3]

The duo apply/update is often a sensible choice, because it gives array-like access on Scala’s side as well, but it is not required to use these names.

Static Module Types

When dealing with modules, ScalaPy offers an additional type StaticModule that makes it possible to map a top-level Scala object to a Python module. For example, to create a static facade to the string module we saw earlier, we can define a StaticModule facade.

@py.native object StringsModule extends py.StaticModule("string") {
def digits: String = py.native
}

StringsModule.digits
// res4: String = "0123456789"

Special Types

Due to Python's dynamically typed nature, some APIs can have types that don't easily map to Scala constructs. To help with this, ScalaPy includes some special types to help defining static types for these situations easier.

py.|

ScalaPy includes the union type py.| which can represent situations where one of two types is required. For example, the Python Random class can be initialized with a seed that is an integer or a string. We could define a type facade as

@py.native trait PythonRandomModule extends py.Object {
def Random(a: py.|[Int, String]): py.Dynamic = py.native
}

And use it with either input type

val random = py.module("random").as[PythonRandomModule]
// random: PythonRandomModule = <module 'random' from '/opt/buildhome/.pyenv/versions/3.9.9/lib/python3.9/random.py'>
random.Random(123)
// res5: py.Dynamic = <random.Random object at 0x7f4e985fbce0>
random.Random("123")
// res6: py.Dynamic = <random.Random object at 0x7f4e985fc6e0>