diff --git a/.editorconfig b/.editorconfig
index 6f74c81..b228e14 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,3 +4,4 @@ root = true
fsharp_multiline_bracket_style = stroustrup
fsharp_record_multiline_formatter = number_of_items
fsharp_max_record_number_of_items = 1
+fsharp_max_infix_operator_expression = 60
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 46de6ed..53894eb 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,6 +2,7 @@
"cSpell.words": [
"aiohttp",
"alfonsogarciacaro",
+ "boto",
"Buildalyzer",
"coro",
"destructures",
@@ -19,9 +20,13 @@
"nativeint",
"ncave",
"pathlib",
+ "pdfplumber",
+ "Plotly",
"pyfile",
"pyname",
+ "Pythonistas",
"Pyxpecto",
+ "scikit",
"Sergey",
"stroustrup",
"Tihon",
diff --git a/CLAUDE.md b/CLAUDE.md
index 8e06c25..9ece319 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -95,4 +95,4 @@ docs/
- [Fable.Python docs](https://fable.io/docs/getting-started/python.html)
- [Fable.Python GitHub](https://github.com/fable-compiler/Fable.Python/)
-- [Content Plan](CONTENT-PLAN.md) - Chapter structure and TODO items
+- [Content Plan](CONTENT-PLAN.md) - Chapter structure and TODO items (important!)
diff --git a/Fable.Literate/App.fs b/Fable.Literate/App.fs
index de70896..f9a5bb7 100644
--- a/Fable.Literate/App.fs
+++ b/Fable.Literate/App.fs
@@ -100,7 +100,9 @@ module Utils =
if m.Value.Length = 1 then
m.Value.ToLowerInvariant()
else
- m.Value.Substring(0, 1) + "_" + m.Value.Substring(1, 1).ToLowerInvariant()
+ m.Value.Substring(0, 1)
+ + "_"
+ + m.Value.Substring(1, 1).ToLowerInvariant()
)
else
name
@@ -328,7 +330,10 @@ module MarkdownPrinter =
| IncludePython symbols ->
// Unresolved - should have been transformed
let symbolList = String.concat ", " symbols
- "\n\n"
+
+ "\n\n"
| Hidden _ -> "" // Should have been filtered
/// Render a document to markdown string.
@@ -348,7 +353,11 @@ module MarkdownPrinter =
| s when s.StartsWith "#" -> inCodeBlock, ("#" + line) :: acc
| _ -> inCodeBlock, line :: acc
- lines |> Array.fold folder (false, []) |> snd |> List.rev |> String.concat "\n"
+ lines
+ |> Array.fold folder (false, [])
+ |> snd
+ |> List.rev
+ |> String.concat "\n"
(**
## Pipeline Module
diff --git a/Fable.Literate/Fable.Literate.fsproj b/Fable.Literate/Fable.Literate.fsproj
index 5022a0a..bb33b37 100644
--- a/Fable.Literate/Fable.Literate.fsproj
+++ b/Fable.Literate/Fable.Literate.fsproj
@@ -8,7 +8,7 @@
-
+
diff --git a/README.md b/README.md
index 78db4c6..5c66420 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
> Write F#, run Python - a practical guide to Fable.Python
-This is a comprehensive guide to [Fable.Python](https://github.com/fable-compiler/Fable.Python/), written as literate F# that transpiles to Python and generates its own documentation.
+This blog-post is a guide to [Fable.Python](https://github.com/fable-compiler/Fable.Python/), written as literate F# that transpiles to Python and generates its own documentation.
## Chapters
@@ -23,9 +23,10 @@ This is a comprehensive guide to [Fable.Python](https://github.com/fable-compile
## The Strange Loop
-This guide is self-documenting: each chapter is an `.fs` file with embedded Markdown comments. **Fabletext** (the final chapter) processes these files to generate the documentation you're reading - including itself.
+This guide is self-documenting: each chapter is an `.fs` file with embedded Markdown comments. **Fable.Literate** (the final chapter) processes these files to generate the documentation you're reading - including itself.
The chain:
+
1. Write F# with embedded Markdown (`chapters/*.fs`)
2. Compile to Python with Fable
3. Run Fabletext (F# compiled to Python) to extract documentation
diff --git a/chapters/AsyncProgramming.fs b/chapters/AsyncProgramming.fs
index a1a01ec..cb2edee 100644
--- a/chapters/AsyncProgramming.fs
+++ b/chapters/AsyncProgramming.fs
@@ -76,7 +76,9 @@ F# async shines when composing multiple operations:
let fetchMultipleAsync () =
async {
- let! results = [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] |> Async.Parallel
+ let! results =
+ [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ]
+ |> Async.Parallel
return results |> Array.toList
}
@@ -398,7 +400,9 @@ When you need rich composition primitives:
let complexWorkflow () =
async {
// Run three operations in parallel
- let! results = [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ] |> Async.Parallel
+ let! results =
+ [ fetchDataAsync (); fetchDataAsync (); fetchDataAsync () ]
+ |> Async.Parallel
// Then do something sequential
do! Async.Sleep 100
diff --git a/chapters/Bindings.fs b/chapters/Bindings.fs
index 12abb02..251abdf 100644
--- a/chapters/Bindings.fs
+++ b/chapters/Bindings.fs
@@ -3,9 +3,13 @@ module Bindings
(**
# Creating Python Bindings
-When a Python library doesn't have F# bindings, you can create your own.
-This chapter covers the patterns and best practices for writing type-safe
-bindings that feel natural in F#.
+When a Python library doesn't have F# bindings, you can create your own. This chapter
+covers the patterns and best practices for writing type-safe bindings that feel natural
+in F#.
+
+> Writing bindings have long been a major pain point, spending countless hours wrestling
+> with interop details. With AI-assisted coding tools, generating initial binding code
+> for your favorite Python libraries has become much easier.
## Core Principles
@@ -273,6 +277,5 @@ let createClient url = myLibrary.createClient url
## What's Next?
-Now you know how to create bindings. The **Compatibility** chapter covers
-which F# features work with Fable.Python and any limitations to be aware of.
+With bindings covered, the **Compatibility** chapter shows which F# features work with Fable.Python and any limitations to be aware of.
*)
diff --git a/chapters/FastAPI.fs b/chapters/FastAPI.fs
index 3ea8962..ea62714 100644
--- a/chapters/FastAPI.fs
+++ b/chapters/FastAPI.fs
@@ -97,27 +97,35 @@ let items = ResizeArray- ()
type API() =
/// GET /items - List all items
[]
- static member get_items() : ResizeArray
- =
- items
+ static member get_items() : ResizeArray
- = items
/// GET /items/{item_id} - Get item by ID
[]
- static member get_item(item_id: int) : Task = task {
- match items |> Seq.tryFind (fun i -> i.Id = item_id) with
- | Some item -> return item :> obj
- | None -> return {| error = "Item not found" |}
- }
+ static member get_item(item_id: int) : Task =
+ task {
+ match items |> Seq.tryFind (fun i -> i.Id = item_id) with
+ | Some item -> return item :> obj
+ | None -> return {| error = "Item not found" |}
+ }
/// POST /items - Create a new item
[]
- static member create_item(request: CreateItemRequest) : Task = task {
- let newId =
- if items.Count = 0 then 1
- else (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1
- let newItem = Item(newId, request.Name, request.Price, request.InStock)
- items.Add(newItem)
- return {| status = "created"; item = newItem |}
- }
+ static member create_item(request: CreateItemRequest) : Task =
+ task {
+ let newId =
+ if items.Count = 0 then
+ 1
+ else
+ (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1
+
+ let newItem = Item(newId, request.Name, request.Price, request.InStock)
+ items.Add(newItem)
+
+ return {|
+ status = "created"
+ item = newItem
+ |}
+ }
(**
This generates Python with proper FastAPI decorators:
@@ -143,8 +151,10 @@ F# anonymous records compile to Python dictionaries, perfect for JSON responses:
[]
type HealthAPI() =
[]
- static member health() =
- {| status = "healthy"; version = "1.0.0" |}
+ static member health() = {|
+ status = "healthy"
+ version = "1.0.0"
+ |}
(**
## Async Endpoints
@@ -155,11 +165,12 @@ For I/O-bound operations, use `task { }` to create async endpoints:
[]
type AsyncAPI() =
[]
- static member slow_operation() = task {
- // Simulate async work (e.g., database query)
- do! Task.Delay(100)
- return {| message = "Done!" |}
- }
+ static member slow_operation() =
+ task {
+ // Simulate async work (e.g., database query)
+ do! Task.Delay(100)
+ return {| message = "Done!" |}
+ }
(**
The `task { }` computation expression compiles to Python's `async def`,
@@ -175,12 +186,16 @@ Path parameters are extracted from the URL:
[]
type UsersAPI() =
[]
- static member get_user(user_id: int) =
- {| id = user_id; name = "User " + string user_id |}
+ static member get_user(user_id: int) = {|
+ id = user_id
+ name = "User " + string user_id
+ |}
[]
- static member get_user_post(user_id: int, post_id: int) =
- {| user_id = user_id; post_id = post_id |}
+ static member get_user_post(user_id: int, post_id: int) = {|
+ user_id = user_id
+ post_id = post_id
+ |}
(**
### Query Parameters
@@ -191,8 +206,10 @@ Query parameters are function arguments not in the path:
[]
type SearchAPI() =
[]
- static member search(q: string, limit: int) =
- {| query = q; limit = limit |}
+ static member search(q: string, limit: int) = {|
+ query = q
+ limit = limit
+ |}
(**
A request to `/search?q=hello&limit=10` maps to `search("hello", 10)`.
@@ -213,7 +230,11 @@ type UserCrudAPI() =
[]
static member create_user(request: CreateUserRequest) =
// FastAPI automatically validates the request body
- {| status = "created"; name = request.name; email = request.email |}
+ {|
+ status = "created"
+ name = request.name
+ email = request.email
+ |}
(**
FastAPI validates the incoming JSON against the Pydantic model and returns
@@ -230,8 +251,10 @@ type ErrorAPI() =
static member protected_route() =
// Check authentication (simplified example)
let isAuthenticated = false
+
if not isAuthenticated then
raise (System.Exception("Not authenticated"))
+
{| message = "Secret data" |}
(**
@@ -336,4 +359,51 @@ This combination gives you:
4. **Familiar ecosystem** - Deploy with standard Python tools
You write type-safe F# code, but deploy and run it like any Python web service.
+
+## Hybrid Architecture: F# Backend with Python Endpoints
+
+Another compelling use case is when you have an existing web service written in F# (using
+ASP.NET Core, Giraffe, or Oxpecker) but need access to the Python ecosystem for specific
+functionality. You can use FastAPI to expose endpoints that leverage Python libraries,
+while your main service remains in F#.
+
+This hybrid approach works well when you need:
+
+### AI/ML Libraries
+
+- **LangChain** / **LlamaIndex** - LLM orchestration and RAG pipelines
+- **Hugging Face Transformers** - Pre-trained models for NLP, vision, audio
+- **OpenAI SDK** / **Anthropic SDK** - LLM API integration with structured outputs
+- **scikit-learn** - Classical machine learning models
+- **PyTorch** / **TensorFlow** - Deep learning inference
+
+### Data Science & Analytics
+
+- **Pandas** / **Polars** - Data manipulation and analysis
+- **NumPy** - Numerical computing
+- **Matplotlib** / **Plotly** - Chart and visualization generation
+- **Apache Arrow** - Efficient cross-language data interchange
+
+### Document Processing
+
+- **PyMuPDF** / **pdfplumber** - PDF text and table extraction
+- **python-docx** - Word document generation
+- **Pillow** - Image processing and manipulation
+- **OpenCV** - Computer vision operations
+
+### Specialized APIs
+
+- **boto3** - AWS services (S3, Lambda, SQS, etc.)
+- **google-cloud-*** - GCP services (BigQuery, Cloud Storage, Vertex AI)
+
+### Scientific Computing
+
+- **SciPy** - Scientific algorithms and optimization
+- **SymPy** - Symbolic mathematics
+- **NetworkX** - Graph algorithms and analysis
+
+The pattern is straightforward: your F# service handles core domain logic and type-safe
+business rules, while specific endpoints delegate to a FastAPI service for capabilities
+where Python dominates. This is especially powerful for AI/ML workloads where the Python
+ecosystem is unmatched.
*)
diff --git a/chapters/GettingStarted.fs b/chapters/GettingStarted.fs
index 4800abd..f3d4068 100644
--- a/chapters/GettingStarted.fs
+++ b/chapters/GettingStarted.fs
@@ -3,7 +3,7 @@ module GettingStarted
(**
# Getting Started with Fable.Python
-Let's set up a Fable.Python project from scratch and get our first F# code running as Python.
+In this section we will set up a Fable.Python project from scratch and get our first F# code running as Python.
## Prerequisites
@@ -12,7 +12,8 @@ You'll need:
- [.NET SDK](https://dotnet.microsoft.com/download) (6.0 or later. We recommend
installing the latest LTS version, currently .NET 10)
- [Python 3.12+](https://www.python.org/downloads/) (Fable targets Python 3.12 or higher)
-- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust
+- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust that
+ simplifies dependency management, virtual environments, and the installation of Python itself.
If you don't have `uv` installed:
@@ -60,8 +61,9 @@ pip install "fable-library==5.0.0a21"
---
-**Note:** Version pinning matters. The fable-library version must match
-your Fable compiler version. PyPI uses `5.0.0a21` format instead of `5.0.0-alpha.21`.
+**Note:** Version pinning matters. The fable-library version must match your Fable
+compiler version. Note that PyPI uses `5.0.0a21` format instead of `5.0.0-alpha.21` for
+prerelease alpha releases.
---
@@ -136,6 +138,6 @@ my-fable-python/
## Next Steps
-Now that you have a working setup, let's explore how to interact with Python
-libraries in the next chapter on **Bindings**.
+Now that you have a working setup, let's see how we can interact with Python
+libraries by using **Bindings**.
*)
diff --git a/chapters/Interop.fs b/chapters/Interop.fs
index ca19dfc..a99aaa2 100644
--- a/chapters/Interop.fs
+++ b/chapters/Interop.fs
@@ -3,8 +3,7 @@ module Interop
(**
# Python Interop
-Now that you have a Fable.Python project set up, let's explore how to work
-with Python libraries and the existing bindings in the Fable.Python ecosystem.
+With a Fable.Python project set up, we can start to work with Python libraries and the existing bindings in the Fable.Python ecosystem.
## The Fable.Python Library
@@ -427,7 +426,7 @@ let loadConfig (path: string) =
(**
## What's Next?
-Now you know how to use existing Python bindings and core interop features.
-In the next chapter, we'll learn how to create your own bindings for
-Python libraries that don't have F# bindings yet.
+Now you know how to use existing Python bindings and core interop features. In the next
+chapter we will see how you can create your own bindings for Python libraries that don't
+have F# bindings yet.
*)
diff --git a/chapters/Introduction.fs b/chapters/Introduction.fs
index dfff1e1..a51897f 100644
--- a/chapters/Introduction.fs
+++ b/chapters/Introduction.fs
@@ -7,7 +7,7 @@ module Introduction
2025](https://sergeytihon.com/2025/11/03/f-advent-calendar-in-english-2025/). Thank you, Sergey Tihon, for organizing
this wonderful tradition that brings the F# community together every year!
-Welcome to this guide on [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) -
+This guide covers [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) -
a compiler that transforms F# code into Python.
## Table of Contents
@@ -21,7 +21,7 @@ a compiler that transforms F# code into Python.
7. [Testing](#heading-testing-fablepython-projects) - Using pytest with F# code
8. [Fable v5](#heading-fable-v5-whats-new) - New features and the Rust core
9. [Pydantic Integration](#heading-pydantic-interop) - Type-safe data validation
-10. [FastAPI](#heading-fastapi) - Building type-safe web APIs
+10. [FastAPI](#heading-fastapi) - Building type-safe web APIs in the Python ecosystem
11. [Units of Measure](#heading-units-of-measure) - Compile-time dimensional analysis
12. [Fable.Literate](#heading-fableliterate-the-strange-loop) - The tool that wrote this post
@@ -42,9 +42,10 @@ F# is a functional-first language with powerful features like:
- **Type inference** - Write less, express more
- **Pattern matching** - Elegant handling of complex data
- **Immutability by default** - Safer, more predictable code
-- **Algebraic data types** - Model your domain precisely
+- **Algebraic data types** - Model your domain precisely with discriminated unions and records
-With Fable.Python, you get all these benefits while targeting the Python ecosystem.
+These features make F# excellent for [Domain Modeling](https://www.pragprog.com/titles/swdddf/domain-modeling-made-functional/) -
+expressing business rules as types that the compiler enforces.
Python is currently [the most popular programming language in the world](https://www.tiobe.com/tiobe-index/). And no
matter what you think of Python, it will always be the second best language for everything. That ubiquity is exactly why
@@ -74,7 +75,7 @@ Fable.Python is a great choice when:
## A First Example
-Let's start with F# code that compiles to Python:
+Let's begin with a simple F# example:
*)
let greet (name: string) = $"Hello, {name}!"
@@ -117,6 +118,6 @@ shape variant, the compiler will warn you about unhandled cases in the `area` fu
## What's Next?
-In the following chapters, we'll explore setting up your environment, working with Python libraries, and understanding
+In the following chapters, we will get started by setting up your environment, working with Python libraries, and understanding
F# compatibility with Fable. Let's begin.
*)
diff --git a/chapters/Python.fs b/chapters/Python.fs
index 7b70d90..e42af88 100644
--- a/chapters/Python.fs
+++ b/chapters/Python.fs
@@ -3,8 +3,7 @@ module ForPythonDevelopers
(**
# Are You a Python Developer?
-If you're coming from Python, welcome. This chapter will help you understand the F# code you'll see throughout this
-guide. F# is more approachable than it might appear, and many concepts are familiar.
+If you're coming from Python, this chapter covers the F# code you'll see throughout this guide. F# is more approachable than it might appear, and many concepts are familiar.
## What is F#?
@@ -16,7 +15,7 @@ is the language you know.
## Key Concepts You'll See
-Let's map F# concepts to Python equivalents you already understand.
+Here's how F# concepts map to Python equivalents you already know.
### Type Inference
@@ -47,7 +46,7 @@ No need to write it unless you want to.
### Pattern Matching
-Python 3.10+ has `match`/`case`. F# pattern matching is similar but more powerful:
+Python 3.10+ has `match`/`case`:
```python
# Python match/case
@@ -59,6 +58,8 @@ match command:
case _:
return unknown_command()
```
+
+F# pattern matching is similar but more powerful:
*)
let handleCommand command =
@@ -159,7 +160,10 @@ let numbers = [ -1; 2; -3; 4; 5 ]
// F# pipeline - reads left to right, top to bottom
let result =
- numbers |> List.filter (fun x -> x > 0) |> List.map (fun x -> x * 2) |> List.sum
+ numbers
+ |> List.filter (fun x -> x > 0)
+ |> List.map (fun x -> x * 2)
+ |> List.sum
(**
The `|>` operator takes the value on the left and passes it as the last
diff --git a/chapters/Summary.fs b/chapters/Summary.fs
index 3d9c834..48e82de 100644
--- a/chapters/Summary.fs
+++ b/chapters/Summary.fs
@@ -49,5 +49,5 @@ from the community make it better for everyone.
- [Fable.Python on GitHub](https://github.com/fable-compiler/Fable.Python/)
- [F# Software Foundation](https://fsharp.org/)
-Welcome to Fable.Python. Now go build something.
+... now go build something.
*)
diff --git a/chapters/Testing.fs b/chapters/Testing.fs
index a004212..5f07fc1 100644
--- a/chapters/Testing.fs
+++ b/chapters/Testing.fs
@@ -235,6 +235,9 @@ project setup:
### Justfile Commands
+We recommend [just](https://github.com/casey/just) as a command runner - it's like
+`make` but simpler and cross-platform. Here's how to set up test commands:
+
```just
# Run tests (.NET)
test:
diff --git a/justfile b/justfile
index ffc82af..abd8f86 100644
--- a/justfile
+++ b/justfile
@@ -59,6 +59,9 @@ generate: build format-python
blogpost: build format-python
#!/usr/bin/env bash
mkdir -p docs
+ # Get Fable version (strip ANSI codes) and timestamp
+ fable_version=$(dotnet fable --version 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' || echo "unknown")
+ timestamp=$(date -u +"%Y-%m-%d %H:%M UTC")
first=true
for name in {{chapters}}; do
# Convert PascalCase to snake_case for Python file naming
@@ -75,6 +78,10 @@ blogpost: build format-python
uv run python output/Fable.Literate/app.py \
--python-file "$pyfile" \
"chapters/${name}.fs" > docs/blogpost.md
+ # Insert version banner after the first heading using awk
+ awk -v ts="$timestamp" -v fv="$fable_version" \
+ 'NR==1 {print; print ""; print "*Generated on " ts " using Fable v" fv "*"; next} {print}' \
+ docs/blogpost.md > docs/blogpost.tmp && mv docs/blogpost.tmp docs/blogpost.md
first=false
else
# Remaining chapters get headers increased by one level