Call Python from Fable
Contents
Call Python from Fable#
Interoperability is a matter of trust between your statically typed F# code and your untyped dynamic JS code. In order to mitigate risks, Fable gives you several possibilities, amongst them type safety through interface contracts. Sometimes it may sound more convenient to just call JS code in a more dynamic fashion, although be aware by doing this potential bugs will not be discovered until runtime.
We’ll describe both the safe way and the dynamic way and then it will be up to you to decide. Let’s start!
Adding the Python library to the project#
The very first thing to do is add the library to our project. Since we always have a pyproject.toml
file, we’ll just
add the wanted library to our project using either poetry add python-library
. The library will then be
available in the .venv
folder.
If your library is in a file, just skip this step.
Type safety with Imports and Interfaces#
To use code from Python libraries first you need to import it into F#.
There are two ways to declare Python imports in Fable: by using either the Import attribute or the import
expressions. The ImportAttribute
can decorate members, types or modules and works as follows:
// Namespace imports
[<Import("*", from="my-module")>]
// import * from "my-module"
// Member imports
[<Import("myFunction", from="my-module")>]
// import { myFunction } from "my-module"
// Default imports
[<Import("default", from="express")>]
// import Express from "express"
You can also use the following alias attributes:
open Fable.Core
open Fable.Core.PyInterop
// Same as Import("*", "my-module")
[<ImportAll("my-module")>]
let myModule: obj = nativeOnly
// Same as Import("default", "my-module")
[<ImportDefault("my-module")>]
let myModuleDefaultExport: obj = nativeOnly
// The member name is taken from decorated value, here `myFunction`
[<ImportMember("my-module")>]
let myFunction(x: int): int = nativeOnly
If the value is globally accessible in JS, you can use the Global
attribute with an optional name parameter instead.
let [<Global>] console: JS.Console = nativeOnly
// You can pass a string argument if you want to use a different name in your F# code
let [<Global("console")>] logger: JS.Console = nativeOnly
Importing relative paths when using an output directory#
If you are putting the generated Python files in an output directory using Fable’s -o
option, be aware that Fable will only move files corresponding to .fs sources, not external files like .py. Fable will automatically adjust relative paths in imports to the location of the generated file, but sometimes it’s difficult to know where the generated file will end up exactly. In these cases, it’s useful to use the ${outDir}
macro which will be replaced by the output directory. For example, if we are importing a Python module like this:
[<ImportDefault("${outDir}/../styles/styles.module.css")>]
let styles: CssModule = nativeOnly
Let’s say we’re compiling with the -o build
option and the file ends up in the build/Components
directory. The generated code will look like:
import styles from "../../styles/styles.module.css";
OOP Class definition and inheritance#
Assuming we need to import a Python class, we can represent it in F# using a standard class declaration with an Import
attribute. In this case we use nativeOnly
as a dummy implementation for its members as the actual implementation will
come from Python. When using nativeOnly
don’t forget to add the return type to the member!
[<Import("DataManager", from="library.data")>]
type DataManager<'Model> (conf: Config) =
member _.delete(data: 'Model): Task<'Model> = nativeOnly
member _.insert(data: 'Model): Task<'Model> = nativeOnly
member _.update(data: 'Model): Task<'Model> = nativeOnly
From this point it is possible to use it or even to inherit from it as it is usually done on regular F#, see the official F# documentation. If you want to inherit and override a member, F# requires you to declare the member as abstract
in the base class, with a default implementation if you want to make it also directly usable from the base class. For example:
// This class lives in Python
[<Import("DataManager", from="library.data")>]
type DataManager<'T> (conf: Config) =
abstract update: data: 'T -> Promise<'T>
default _.update(data: 'T): Promise<'T> = nativeOnly
// This class lives in our code
type MyDataManager<'T>(config) =
inherit DataManager<'T>(config)
// We can do something with data before sending it to the base class
override _.update(data) =
base.update(data)
let test (data: 'T) (manager: DataManager<'T>) =
manager.update(data) |> ignore
// This will call MyDataManager.update even if test expects DataManager
MyDataManager(myConfig) |> test myData
Note
If you have a Python type declaration for the Python code you want to import, you can use ts2fable to help you write the F# bindings.
Let’s practice! 1st try!#
Let’s say we have an alert.py
file that we’d like to use in our Fable project.
def trigger_alert(message):
print(message)
some_string = "And I Like that!"
__all__ = ["trigger_alert", "some_string"]
As you can see we’ve got one function, trigger_alert
and one constant some_string
. Both are exported using the __all__
keyword.
In order to use this in our Fable code, let’s create an interface
that will mimic this:
open Fable.Core.PyInterop // needed to call interop tools
[<Erase>]
type IAlert =
abstract triggerAlert : message:string -> unit
abstract someString: string
As you can see the process is quite easy. The I
in IAlert
is not mandatory but it’s a precious hint that we’re going
to use an interface. The abstract
keyword only indicates that there’s no actual implementation in F#. That’s true,
since we rely on the Python one. The Erase
attribute is used to remove the interface from the generated code. This is
different from Fable (JavaScript) where interfaces are not present in the generated code.
Now let’s use this:
[<ImportAll("alert")>]
let mylib: IAlert = nativeOnly
Here we use the ImportAll
attribute, to import the module.
step 1: We specify the module we wish to use. Here
alert
means: “import thealert.py
file”.step 2: we create a let binding called
mylib
to map the js library.step 3: we use the
nativeOnly
keyword to say thatmylib
is just a placeholder for the Python native implementation.
Now we can use this:
mylib.triggerAlert ("Hey I'm calling my js library from Fable > " + mylib.someString)
If everything’s working correctly, it should see the output in your console!
Let’s practice! 2nd try!#
Assuming we’ve got two exported functions from a canvas.py
file: draw_smiley
and draw_bubble
:
def draw_smiley(id):
# do something
def draw_bubble(id):
# do something
we could use the same method we used with alert.py
:
open Fable.Core.PyInterop // needed to call interop tools
type ICanvas =
abstract drawSmiley: (id:string) -> unit
abstract drawBubble: (id:string) -> unit
[<ImportAll("canvas")>]
let mylib: ICanvas = nativeOnly
mylib.drawSmiley "smiley" // etc..
or we could use the importMember
helper function to directly map the Python function to the F# function.
open Fable.Core.PyInterop // needed to call interop tools
module Canvas =
// here we just import a member function from canvas.js called drawSmiley.
let drawSmiley(id:string): unit = importMember "canvas"
let drawBubble(id:string): unit = importMember "canvas"
Canvas.drawSmiley "smiley"
The result would be the same, but the philosophy is slightly different. That’s basically up to you to make a choice 😉
Miscellaneous import helpers#
There are other interop helpers you can use thanks to Fable.Core.PyInterop
:
open Fable.Core.PyInterop
let buttons: obj = importAll "my_lib.buttons"
// Py: import my_lib.buttons as my_lib
let myString: string = importMember "my_lib"
// Py: from my_lib import my_string
// Use just `import` to make the member name explicit
// as with the ImportAttribute
let aDifferentName: string = import "myString" "my-lib"
// Py: from my_lib import my_string
Occasionally, you may need to import Python purely for its side-effects.
For this, use importSideEffects
.
open Fable.Core.PyInterop
importSideEffects("mylibrary")
Emit, when F# is not enough#
You can use the Emit
attribute to decorate a function. Every call to the function will then be replaced inline by the content of the attribute with the placeholders $0, $1, $2...
replaced by the arguments. For example, the following code will generate Python as seen below.
open Fable.Core
[<Emit("$0 + $1")>]
let add (x: int) (y: string): string = nativeOnly
let result = add 1 "2"
When you don’t know the exact number of arguments you can use the following syntax:
type Test() =
[<Emit("$0($1...)")>]
member __.Invoke([<ParamArray>] args: int[]): obj = nativeOnly
It’s also possible to pass syntax conditioned to optional parameters:
type Test() =
[<Emit("$0[$1]{{=$2}}")>]
member __.Item with get(): float = nativeOnly and set(v: float): unit = nativeOnly
// This syntax means: if second arg evals to true in JS print 'i' and nothing otherwise
[<Emit("new RegExp($0,'g{{$1?i:}}')")>]
member __.ParseRegex(pattern: string, ?ignoreCase: bool): Regex = nativeOnly
The content of Emit
will not be checked by the F# compiler so it’s not advised to abuse this method.
Let’s do it! Use Emit#
Now let’s work with Emit and look at a new example with the following my_class.py
:
import math
class MyClass:
# Note the constructor accepts an object
# with the `value` and `awesomeness` fields
def __init__(self, value, awesomeness):
self._value = value
self._awesomeness = awesomeness
@property
def value()
return this._value
@value.setter
def set_value(self, new_alue)
self._value = new_value
def is_awesome(self):
return this._value == this._awesomeness
@staticmethod
def getPI():
return math.pi
Let’s list its members:
a
value
property member which returns the current value with a getter and a settera method,
is_awesome
, that checks if the current value equals the awesome valuea static method
getPi()
that just returns the value ofmath.pi
Here’s the Fable implementation. Let’s start with the members:
type MyClass<'T> =
// Note we specify this property has also a setter
abstract value: 'T with get, set
abstract isAwesome: unit -> bool
Now we need to be able to call the static functions, including the constructor. So we write another interface for that:
type MyClassStatic =
[<Emit("$0(value=$1, awesomeness=$2)")>]
abstract Create: 'T * 'T -> MyClass<'T>
abstract getPI : unit-> float
Note
We could have used a class declaration with dummy implementations as we did with DataManager above, but you will find that in Fable bindings it’s common to split the instance and static parts of a Python type in two interfaces to overcome some restrictions of the F# type system or to be able to deal with Python classes as values. In this case, by convention Create
denotes the constructor.
Here we used the Emit
attribute to build a Python object with the arguments, because that’s what MyClass constructor accepts. Note that here $0
represents the interface object (in this case, MyClass static).
Last but not least, let’s import MyClass:
[<Import("MyClass", "public")>]
let MyClass : MyClassStatic = nativeOnly
Now it’s possible to use our Python class. Let’s see the complete code:
type MyClass<'T> =
abstract value: 'T with get, set
abstract isAwesome: unit -> bool
type MyClassStatic =
[<Emit("$0(value=$1, awesomeness=$2)")>]
abstract Create: 'T * 'T -> MyClass<'T>
abstract getPI : unit-> float
[<Import("MyClass", "public")>]
let MyClass : MyClassStatic = nativeOnly
let myObject = MyClass.Create(40, 42)
// using getter
let whatDoIget = myObject.value // using getter
mylib.triggerAlert ("Hey I'm calling my js class from Fable. It gives " + (string whatDoIget))
// using setter
myObject.value <- 42
mylib.triggerAlert ("Now it's better. It gives " + (string myObject.value))
// calling member function
mylib.triggerAlert ("Isn't it awesome? " + (string (myObject.isAwesome())))
// call our static function
mylib.triggerAlert ("PI is " + (string (MyClass.getPI())))
It’s possible to combine the Import
and Emit
attributes. So we can import and build MyClass in one go. Note that in
this case $0
is replaced by the imported element:
[<Import("MyClass", "public")>]
[<Emit("$0(value=$1, awesomeness=$2)")>]
let createMyClass(value: 'T, awesomeness: 'T) : MyClass<'T> = nativeOnly
Other special attributes#
Fable.Core
includes other attributes for Python interop, like:
Erase attribute#
In TypeScript there’s a concept of Union
Types which differs from union types in
F#. The former are just used to statically check a function argument accepting different types. In Fable, they’re
translated as Erased Union Types whose cases must have one and only one single data field. After compilation, the
wrapping will be erased and only the data field will remain. To define an erased union type, just attach the Erase
attribute to the type. Example:
open Fable.Core
[<Erase>]
type MyErasedType =
| String of string
| Number of int
myLib.myMethod(String "test")
myLib.myMethod("test");
Fable.Core
already includes predefined erased types which can be used as follows:
open Fable.Core
type Test = abstract Value: string
let myMethod(arg: U3<string, int, Test>): unit = importMember "my_py_lib"
let testValue = { new Test with member __.Value = "Test" }
myMethod(U3.Case3 testValue)
When passing arguments to a method accepting U2
, U3
… you can use the !^
as syntax sugar so you don’t need to type the exact case (the argument will still be type checked):
open Fable.Core.PyInterop
myMethod !^5 // Equivalent to: myMethod(U3.Case2 5)
myMethod !^testValue
// This doesn't compile, myMethod doesn't accept floats
myMethod !^2.3
Note
Please note erased unions are mainly intended for typing the signature of imported JS functions and not as a cheap replacement of Choice
. It’s possible to do pattern matching against an erased union type but this will be compiled as type testing, and since type testing is very weak in Fable, this is only recommended if the generic arguments of the erased union are types that can be easily told apart in the JS runtime (like a string, a number and an array).
let test(arg: U3<string, int, float[]>) =
match arg with
| U3.Case1 x -> printfn "A string %s" x
| U3.Case2 x -> printfn "An int %i" x
| U3.Case3 xs -> Array.sum xs |> printfn "An array with sum %f"
// In JS roughly translated as:
// function test(arg) {
// if (typeof arg === "number") {
// toConsole(printf("An int %i"))(arg);
// } else if (isArray(arg)) {
// toConsole(printf("An array with sum %f"))(sum(arg));
// } else {
// toConsole(printf("A string %s"))(arg);
// }
// }
StringEnum#
In TypeScript it is possible to define String Literal Types which are similar to enumerations with an underlying string value. Fable allows the same feature by using union types and the StringEnum
attribute. These union types must not have any data fields as they will be compiled to a string matching the name of the union case.
By default, the compiled string will have the first letter lowered. If you want to prevent this or use a different text than the union case name, use the CompiledName
attribute:
open Fable.Core
[<StringEnum>]
type MyStrings =
| Vertical
| [<CompiledName("Horizontal")>] Horizontal
myLib.myMethod(Vertical, Horizontal)
// Python output
myLib.myMethod("vertical", "Horizontal")
Plain Old JavaScript Objects#
To create plain Python dictionaries, use createObj
:
open Fable.Core.PyInterop
let data =
createObj [
"todos" ==> Storage.fetch()
"editedTodo" ==> Option<Todo>.None
"visibility" ==> "all"
]
A similar effect can be achieved with the new F# anonymous records:
let data =
{| todos = Storage.fetch()
editedTodo = Option<Todo>.None
visibility = "all" |}
Note
Since fable-compiler 2.3.6, when using the dynamic cast operator !!
to cast an anonymous record to an interface, Fable will raise a warning if the fields in the anonymous don’t match those of the interface. Use this feature only to interop with Python, in F# code the proper way to instantiate an interface without an implementing type is an object expression.
type IMyInterface =
abstract foo: string with get, set
abstract bar: float with get, set
abstract baz: int option with get, set
// Warning, "foo" must be a string
let x: IMyInterface = !!{| foo = 5; bar = 4.; baz = Some 0 |}
// Warning, "bar" field is missing
let y: IMyInterface = !!{| foo = "5"; bAr = 4.; baz = Some 0 |}
// Ok, "baz" can be missing because it's optional
let z: IMyInterface = !!{| foo = "5"; bar = 4. |}
You can also create a Python dictinary from an interface by using createEmpty
and then assigning the fields manually:
let x = createEmpty<IMyInterface> // var x = {}
x.foo <- "abc" // x.foo = "abc"
x.bar <- 8.5 // val.bar = 8.5
A similar solution that can also be optimized by Fable directly into a JS object at compile time is to use the jsOptions
helper:
let x = pyOptions<IMyInterface>(fun x ->
x.foo <- "abc"
x.bar <- 8.5)
Another option is to use a list (or any sequence) of union cases in combination with the keyValueList
helper. This is often used to represent React props. You can specify the case rules for the transformations of the case names (usually lowering the first letter) and if necessary you can also decorate some cases with the CompiledName
attribute to change its name in the JS runtime.
open Fable.Core.PyInterop
type PyOption =
| Flag1
| Name of string
| [<CompiledName("quantity")>] QTY of int
let inline sendToPy (opts: PyOption list) =
keyValueList CaseRules.LowerFirst opts |> aNativePyFunction
sendToPy [
Flag1
Name "foo"
QTY 5
]
// Py: {'flag1': True, 'name': 'foo'}
Note
Fable can make the transformation at compile time when applying the list literal directly to keyValueList
. That’s why
it’s usually a good idea to inline the function containing the helper.
Dynamic typing: don’t read this!#
Through the use of the tools we just described above, Fable guarantees you shouldn’t run into nasty bugs (as long as the interface contracts are correct) because all the code will be checked by the compiler. If it does not compile it either means your Python library does not exist or its path is not good or that your F# implementation lacks something. We do rely on Fable on systems that are used 24/7. We know that if it compiles, it means a 99% chance of running without any problems.
Our motto is: “If it compiles, it works!”
Still, like we stated, interop is a question of trust. If you trust your Python code and F# code, then maybe it’s ok to do things together without further checks. Maybe.
Warning
Disclaimer: use this at your own risk
What is dynamic typing?#
Fable.Core.PyInterop implements the F# dynamic operators so you can easily access an object property by name (without static check) as follows:
open Fable.Core.PyInterop
printfn "Value: %O" pyObject?myProperty
// Py: py_object.myProperty
let pname = "myProperty"
printfn "Value: %O" jsObject?(pname) // Access with a reference
// Py: py_object[pname]
pyObject?myProperty <- 5 // Assignment is also possible
When you combine the dynamic operator with application, Fable will destructure tuple arguments as with normal method calls. These operations can also be chained to replicate Python fluent APIs.
let result = pyObject?myMethod(1, 2)
// Py: py_object.my_method(1, 2)
chart
?width(768.)
?height(480.)
?group(speedSumGroup)
// (chart
// .width(768)
// .height(480)
// .group(speedSumGroup))
If you prefer member extensions rather than operators for dynamic typing, you can open Fable.Core.DynamicExtensions
to
have the methods .Item
and .Invoke
available on any object.
open Fable.Core.DynamicExtensions
let foo = obj()
let bar1 = foo.["b"] // Same as foo.Item("b")
foo.["c"] <- 14
let bar2 = foo.Invoke(4, "a")
Dynamic casting#
In some situations, when receiving an untyped object from Python you may want to cast it to a specific type. For this
you can use the F# unbox
function or the !!
operator in Fable.Core.PyInterop. This will bypass the F# type checker
but please note Fable will not add any runtime check to verify the cast is correct.