This guide documents best practices for writing F# type bindings for Python libraries in Fable.Python, based on the Glutinum project's insights and the existing patterns in this repository.
- Core Principles
- Import Patterns
- Type System Mappings
- Fable.Core Attributes Reference
- Common Patterns
- Best Practices
- Testing Your Bindings
- Examples
When writing Fable.Python bindings, follow these guiding principles:
- Provide near-native F# experience while staying close to the original Python API
- Minimize friction by avoiding erased union types (U2, U3, etc.) - use function overloads instead
- Maintain documentation compatibility - users should be able to reference Python documentation
- Type safety first - leverage F#'s type system to catch errors at compile time
- Follow F# conventions - use camelCase for F# identifiers (Fable auto-converts to snake_case)
The recommended pattern for importing Python modules:
[<Erase>]
type IExports =
abstract function_name: param: type -> returnType
abstract another_function: param1: type1 * param2: type2 -> returnType
[<ImportAll("module_name")>]
let moduleName: IExports = nativeOnlyWhat this generates:
import module_nameImportant: [<ImportAll("module")>] generates import module, NOT from module import *
For Python classes that need to be instantiated or inherited:
[<Import("ClassName", "module_name")>]
type ClassName =
abstract member property: type
abstract member method: param: type -> returnTypeWhat this generates:
from module_name import ClassNameFor Python global constants or variables:
[<Global>]
let __name__: string = nativeOnlyOr use [<Emit>] for special cases:
[<Emit("__name__")>]
let __name__: string = nativeOnlyUnderstanding how .NET/F# types map to Python:
| F# Type | Python Type | Notes |
|---|---|---|
string |
str |
Direct mapping |
bool |
bool |
Direct mapping |
int |
int |
Custom PyO3 wrapper maintains F# semantics |
float |
float |
Custom PyO3 wrapper maintains F# semantics |
char |
str |
Single-character string |
unit |
None |
Void/None value |
'T option |
T | None |
Erased - Some 5 becomes just 5 |
'T array |
list[T] |
Mutable array |
'T list |
Various | Immutable linked list (custom) |
ResizeArray<'T> |
list[T] |
Python's native list |
seq<'T> / IEnumerable<'T> |
iterable | Any iterable |
| F# Type | Python Type | Notes |
|---|---|---|
| Records | dataclass |
Compiled to Python dataclasses |
| Anonymous records | dict |
Dictionary with string keys |
| Discriminated unions | Various | Depends on attributes (see StringEnum) |
| Interfaces | Protocols | Maps to Python protocols/ABCs |
| Tuples | tuple |
Native Python tuples |
| .NET Interface | Python Protocol | Generated Methods |
|---|---|---|
IDisposable |
Context manager | __enter__, __exit__ |
IEnumerable<'T> |
Iterator | __iter__ |
IEquatable<'T> |
Equality | __eq__ |
IComparable<'T> |
Comparison | __lt__, __eq__ |
Imports the entire module and binds it to an F# value.
[<ImportAll("json")>]
let json: IExports = nativeOnlyGenerates: import json
Imports a specific member from a module.
[<Import("Flask", "flask")>]
let Flask: FlaskStatic = nativeOnlyGenerates: from flask import Flask
Imports a member whose name matches the F# value name.
[<ImportMember("datetime")>]
let datetime: DateTimeStatic = nativeOnlyGenerates: from datetime import datetime
References a global Python variable without import.
[<Global>]
let __name__: string = nativeOnlyPrevents code generation for the type - it's only used at compile time.
Use for:
- Module export interfaces (IExports pattern)
- DSL types that compile away
- Virtual types for API organization
[<Erase>]
type IExports =
abstract dumps: obj: obj -> stringImportant: The Glutinum blog post advises minimizing erased unions (U2, U3), but using [<Erase>] for module export interfaces is acceptable and idiomatic for Fable.Python.
Compiles discriminated unions to string literals.
[<StringEnum>]
[<RequireQualifiedAccess>]
type HttpMethod =
| [<CompiledName("GET")>] Get
| [<CompiledName("POST")>] Post
| Put // Compiles to "put" with default CaseRulesCase Rules:
CaseRules.None- Use exact caseCaseRules.LowerFirst- Default: lowerFirstCaseRules.SnakeCase- snake_caseCaseRules.KebabCase- kebab-case
Use for:
- String-based enumerations
- API parameters that accept specific string values
- Mode flags and options
Allows the type to be null/None.
[<AllowNullLiteral>]
type OptionalObject =
abstract property: stringRequires qualified access to union cases or module members.
[<StringEnum>]
[<RequireQualifiedAccess>]
type FileMode =
| Read
| Write
| Append
// Usage: FileMode.Read instead of just ReadBest practice: Always use with [<StringEnum>] to avoid polluting the namespace.
Directly emits Python code. Placeholders $0, $1, $2 represent arguments.
// For special syntax cases
[<Emit("$0.get_running_loop()")>]
abstract get_running_loop: unit -> AbstractEventLoop
// For custom operators
[<Emit("$0 if $1 else $2")>]
let inline ternary condition whenTrue whenFalse = nativeOnlyUse sparingly for:
- Python-specific syntax not expressible in F#
- Special operators or constructs
- Named arguments with special positioning
Converts parameters starting from index n to Python keyword arguments.
type IExports =
[<NamedParams(fromIndex = 1)>]
abstract open:
file: string *
?mode: string *
?encoding: string ->
TextIOWrapperGenerates: open(file, mode=..., encoding=...)
Use for:
- Python functions with many optional parameters
- Functions where parameter order is important but some are optional
Applies Python decorators to classes or functions.
[<Py.Decorator("dataclass")>]
type MyClass = ...Generates:
@dataclass
class MyClass:
...Controls how class members are generated.
Attaches all members directly to the class prototype. Disables overload support.
Forces name mangling on interfaces for overload safety.
For modules that export only functions (e.g., json, time, os):
module Fable.Python.ModuleName
open Fable.Core
[<Erase>]
type IExports =
/// Function documentation from Python docs
abstract function_name: param: type -> returnType
/// Function with multiple parameters
abstract another_function: param1: type1 * param2: type2 -> returnType
/// Module description from Python docs
[<ImportAll("module_name")>]
let moduleName: IExports = nativeOnlyFor modules that export classes (e.g., ast, datetime):
module Fable.Python.ModuleName
open Fable.Core
// Import the class
[<Import("ClassName", "module_name")>]
type ClassName =
abstract property: type
abstract method: param: type -> returnType
// Import the module for other functions
[<Erase>]
type IExports =
abstract module_function: param: type -> returnType
[<ImportAll("module_name")>]
let moduleName: IExports = nativeOnlyDO THIS (multiple overloads):
[<Erase>]
type IExports =
/// Parse a string
abstract parse: source: string -> AST
/// Parse a string with filename
abstract parse: source: string * filename: string -> AST
/// Parse a string with filename and mode
abstract parse: source: string * filename: string * mode: Mode -> ASTNOT THIS (erased unions):
// ❌ Avoid this pattern - creates friction
abstract parse: source: string * options: U2<string, string * Mode> -> ASTFor Python parameters that accept specific string values:
[<StringEnum>]
[<RequireQualifiedAccess>]
type FileMode =
| [<CompiledName("r")>] Read
| [<CompiledName("w")>] Write
| [<CompiledName("a")>] Append
| [<CompiledName("r+")>] ReadUpdate
[<Erase>]
type IExports =
abstract open: path: string * mode: FileMode -> FileUsage in F#:
file.open("data.txt", FileMode.Read)Generates in Python:
file.open("data.txt", "r")F# optional parameters work naturally:
[<Erase>]
type IExports =
abstract open: path: string * ?mode: string * ?encoding: string -> FileProvide F#-friendly wrappers for common operations:
[<ImportAll("builtins")>]
let builtins: IExports = nativeOnly
// Convenience wrapper for common use
let print obj = builtins.print objFor complex or commonly-used types:
type _identifier = string
type _Opener = Tuple<string, int> -> int
[<Erase>]
type IExports =
abstract get_identifier: unit -> _identifier
abstract open: path: string * opener: _Opener -> FileAlways include XML documentation comments from the Python documentation:
[<Erase>]
type IExports =
/// Return the absolute value of the argument.
abstract abs: int -> intFollow the established pattern:
- Stdlib modules:
Fable.Python.ModuleName - Third-party libraries:
Fable.Python.LibraryName
// 1. Module declaration
module Fable.Python.ModuleName
// 2. Open statements
open System
open Fable.Core
// 3. Disable linting for Python naming conventions
// fsharplint:disable MemberNames,InterfaceNames
// 4. Type aliases
type _identifier = string
// 5. Class/type imports
[<Import("ClassName", "module")>]
type ClassName = ...
// 6. Module exports interface
[<Erase>]
type IExports = ...
// 7. Module import
[<ImportAll("module")>]
let moduleName: IExports = nativeOnly
// 8. Convenience wrappers (if any)
let wrapper x = moduleName.function x- F# identifiers: Use
camelCase- Fable automatically converts tosnake_case - Special Python names: Use backticks for F# keywords:
open,module - Preserve Python semantics: Keep parameter names close to Python documentation
When Python accepts different argument types:
-
Same type, different arities: Use multiple overloads
abstract parse: string -> AST abstract parse: string * filename: string -> AST
-
Different types, same arity: Use multiple overloads
abstract abs: int -> int abstract abs: float -> float
-
Complex variations: Consider separate function names or StringEnum for mode flags
For truly dynamic APIs, use these escape hatches:
// Dynamic property access
let value = pythonObject?propertyName
// Dynamic method calls
pythonObject?method(arg1, arg2)
// Dynamic setting
pythonObject?property <- value
// Type casting (unsafe)
let typed: SomeType = unbox pythonObjectUse sparingly - prefer typed bindings when possible.
Python classes often inherit from others:
// Base type
[<Import("TextIOBase", "io")>]
type TextIOBase =
abstract read: unit -> string
abstract write: s: string -> int
// Derived type
[<Import("TextIOWrapper", "io")>]
type TextIOWrapper =
inherit TextIOBase
inherit System.IDisposable // Adds context manager supportDON'T:
- ❌ Use erased union types (U2, U3) unless absolutely necessary
- ❌ Forget
nativeOnlyafter import declarations - ❌ Mix different import patterns in the same binding
- ❌ Ignore Python's naming conventions
- ❌ Over-use
[<Emit>]when proper attributes exist
DO:
- ✅ Use multiple overloads instead of union parameters
- ✅ Use
[<StringEnum>]for string-based enumerations - ✅ Keep bindings focused and coherent
- ✅ Document all public APIs
- ✅ Test your bindings with actual Python code
Create test files in the test/ directory:
module Tests.ModuleName
open Xunit
open Fable.Python.ModuleName
[<Fact>]
let ``module should parse simple expression`` () =
let result = moduleName.parse "1 + 1"
// Assertions...# Compile F# tests to Python
dotnet fable --lang Python --outDir build/tests test
# Run with pytest
uv run pytest build/testsCheck the compiled Python in build/tests/ to ensure:
- Imports are correct
- Function calls use proper Python syntax
- Snake_case conversion is applied correctly
module Fable.Python.Json
open Fable.Core
// fsharplint:disable MemberNames
[<Erase>]
type IExports =
/// Serialize obj to a JSON formatted string
abstract dumps: obj: obj -> string
/// Deserialize s (a string instance containing a JSON document) to a Python object
abstract loads: s: string -> obj
/// JSON encoder and decoder
[<ImportAll("json")>]
let json: IExports = nativeOnlymodule rec Fable.Python.Ast
open Fable.Core
// fsharplint:disable MemberNames,InterfaceNames
type _identifier = string
// Base class for all AST nodes
[<Import("AST", "ast")>]
type AST =
abstract foo: int
// Specific node types
[<Import("Module", "ast")>]
type Module =
inherit AST
abstract body: stmt array
[<Import("stmt", "ast")>]
type stmt =
inherit AST
// String enumeration for modes
[<StringEnum>]
[<RequireQualifiedAccess>]
type Mode =
| [<CompiledName("exec")>] Exec
| [<CompiledName("eval")>] Eval
// Module functions
[<Erase>]
type IExports =
/// Parse the source into an AST node
abstract parse: source: string -> AST
abstract parse: source: string * filename: string -> AST
abstract parse: source: string * filename: string * mode: Mode -> AST
/// Convert an AST back to Python code
abstract unparse: astObj: AST -> string
/// Abstract Syntax Trees
[<ImportAll("ast")>]
let ast: IExports = nativeOnlymodule Fable.Python.Builtins
open System
open Fable.Core
// fsharplint:disable MemberNames
type TextIOBase =
abstract read: unit -> string
abstract read: size: int -> string
abstract write: s: string -> int
type TextIOWrapper =
inherit IDisposable
inherit TextIOBase
type _Opener = Tuple<string, int> -> int
[<Erase>]
type IExports =
/// Return the absolute value of the argument
abstract abs: int -> int
/// Return the absolute value of the argument
abstract abs: float -> float
/// Return the length of an object
abstract len: obj -> int
/// Open file and return a stream
[<NamedParams(fromIndex = 1)>]
abstract open:
file: string *
?mode: string *
?buffering: int *
?encoding: string *
?errors: string *
?newline: string *
?closefd: bool *
?opener: _Opener ->
TextIOWrapper
[<ImportAll("builtins")>]
let builtins: IExports = nativeOnly
// Convenience wrapper
let print obj = builtins.print objmodule Fable.Python.Flask
open Fable.Core
// fsharplint:disable MemberNames
type Request =
abstract url: string
abstract method: string
abstract headers: obj
type Flask =
/// Register a route handler
abstract route: rule: string -> ((unit -> string) -> Flask)
/// Register a route handler with HTTP methods
abstract route: rule: string * methods: string array -> ((unit -> string) -> Flask)
type FlaskStatic =
/// Create a Flask application
[<Emit("$0($1, static_url_path=$2)")>]
abstract Create: name: string * static_url_path: string -> Flask
[<Import("Flask", "flask")>]
let Flask: FlaskStatic = nativeOnly
[<Erase>]
type IExports =
/// Render a template
abstract render_template: template_name: string -> string
/// The request object
abstract request: Request
/// Generate a URL for the given endpoint
[<Emit("flask.url_for($0, filename=$1)")>]
abstract url_for: endpoint: string * filename: string -> string
[<ImportAll("flask")>]
let flask: IExports = nativeOnlyBefore submitting new bindings:
- Package is publicly available on PyPI
- Package supports Python 3.12+ (Fable 5 requirement)
- Package doesn't ship with its own type stubs
- Bindings follow the standard patterns documented here
- All public APIs have XML documentation
- Tests are written and passing
- File is added to
src/Fable.Python.fsprojin correct order - Bindings are in the correct directory (
src/stdlib/orsrc/<library>/) - Code follows F# naming conventions (camelCase)
- No erased union types (U2, U3) unless absolutely necessary
- FSharpLint issues are addressed or suppressed with good reason
- Fable Python Documentation
- Glutinum: A New Era for Fable Bindings
- Glutinum CLI
- Python Documentation
- Fable.Python Repository
Writing effective Fable.Python bindings requires:
- Understanding the Python API - Read the official docs thoroughly
- Following established patterns - Use the IExports + ImportAll pattern
- Leveraging F#'s type system - Make invalid states unrepresentable
- Prioritizing developer experience - Avoid erased unions, use overloads
- Testing thoroughly - Both native F# tests and compiled Python tests
The goal is to provide a near-native F# experience that allows developers to use Python libraries with all the benefits of F#'s type safety, while maintaining compatibility with Python's documentation and idioms.